From b5e6eef1185d7fbdc0de154732b87b884a0819d3 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 26 Sep 2025 16:51:15 -0700 Subject: [PATCH 1/3] improvement(copilot): added session context checks in copilot tool calls --- .../execute-copilot-server-tool/route.ts | 2 +- apps/sim/lib/copilot/auth/permissions.ts | 99 +++++++++++++++++++ .../sim/lib/copilot/tools/server/base-tool.ts | 2 +- apps/sim/lib/copilot/tools/server/router.ts | 9 +- .../server/user/get-environment-variables.ts | 43 +++++--- .../server/user/get-oauth-credentials.ts | 38 +++++-- .../server/user/set-environment-variables.ts | 39 ++++++-- 7 files changed, 195 insertions(+), 37 deletions(-) create mode 100644 apps/sim/lib/copilot/auth/permissions.ts diff --git a/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts b/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts index d356162857..333e5bed43 100644 --- a/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts +++ b/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts @@ -34,7 +34,7 @@ export async function POST(req: NextRequest) { const { toolName, payload } = ExecuteSchema.parse(body) logger.info(`[${tracker.requestId}] Executing server tool`, { toolName }) - const result = await routeExecution(toolName, payload) + const result = await routeExecution(toolName, payload, { userId }) try { const resultPreview = JSON.stringify(result).slice(0, 300) diff --git a/apps/sim/lib/copilot/auth/permissions.ts b/apps/sim/lib/copilot/auth/permissions.ts new file mode 100644 index 0000000000..e20f622295 --- /dev/null +++ b/apps/sim/lib/copilot/auth/permissions.ts @@ -0,0 +1,99 @@ +import { db } from '@sim/db' +import { workflow } from '@sim/db/schema' +import { eq } from 'drizzle-orm' +import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions, type PermissionType } from '@/lib/permissions/utils' + +const logger = createLogger('CopilotPermissions') + +/** + * Verifies if a user has access to a workflow for copilot operations + * Follows the same authorization pattern as /api/workflows/[id]/route.ts + * + * @param userId - The authenticated user ID + * @param workflowId - The workflow ID to check access for + * @returns Promise<{ hasAccess: boolean; userPermission: PermissionType | null; workspaceId?: string; isOwner: boolean }> + */ +export async function verifyWorkflowAccess( + userId: string, + workflowId: string +): Promise<{ + hasAccess: boolean + userPermission: PermissionType | null + workspaceId?: string + isOwner: boolean +}> { + try { + // Fetch the workflow to get its workspace and owner info (same query as workflow API) + const workflowData = await db + .select({ + userId: workflow.userId, + workspaceId: workflow.workspaceId, + }) + .from(workflow) + .where(eq(workflow.id, workflowId)) + .limit(1) + + if (!workflowData.length) { + logger.warn('Attempt to access non-existent workflow', { + workflowId, + userId, + }) + return { hasAccess: false, userPermission: null, isOwner: false } + } + + const { userId: workflowOwnerId, workspaceId } = workflowData[0] + + if (workflowOwnerId === userId) { + logger.debug('User has direct ownership of workflow', { workflowId, userId }) + return { + hasAccess: true, + userPermission: 'admin', + workspaceId: workspaceId || undefined, + isOwner: true, + } + } + + if (workspaceId && userId) { + const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + + if (userPermission !== null) { + logger.debug('User has workspace permission for workflow', { + workflowId, + userId, + workspaceId, + userPermission, + }) + return { + hasAccess: true, + userPermission, + workspaceId: workspaceId || undefined, + isOwner: false, + } + } + } + + logger.warn('User has no access to workflow', { + workflowId, + userId, + workspaceId, + workflowOwnerId, + }) + return { + hasAccess: false, + userPermission: null, + workspaceId: workspaceId || undefined, + isOwner: false, + } + } catch (error) { + logger.error('Error verifying workflow access', { error, workflowId, userId }) + return { hasAccess: false, userPermission: null, isOwner: false } + } +} + +/** + * Helper function to create consistent permission error messages + */ +export function createPermissionError(operation: string): string { + return `Access denied: You do not have permission to ${operation} this workflow` +} diff --git a/apps/sim/lib/copilot/tools/server/base-tool.ts b/apps/sim/lib/copilot/tools/server/base-tool.ts index 5affc7a5b1..40ec3584cb 100644 --- a/apps/sim/lib/copilot/tools/server/base-tool.ts +++ b/apps/sim/lib/copilot/tools/server/base-tool.ts @@ -1,4 +1,4 @@ export interface BaseServerTool { name: string - execute(args: TArgs): Promise + execute(args: TArgs, context?: { userId: string }): Promise } diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index ab59fc9313..fbf7d44396 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -50,8 +50,11 @@ serverToolRegistry[readGDriveFileServerTool.name] = readGDriveFileServerTool serverToolRegistry[getOAuthCredentialsServerTool.name] = getOAuthCredentialsServerTool serverToolRegistry[makeApiRequestServerTool.name] = makeApiRequestServerTool -// Main router function -export async function routeExecution(toolName: string, payload: unknown): Promise { +export async function routeExecution( + toolName: string, + payload: unknown, + context?: { userId: string } +): Promise { const tool = serverToolRegistry[toolName] if (!tool) { throw new Error(`Unknown server tool: ${toolName}`) @@ -81,7 +84,7 @@ export async function routeExecution(toolName: string, payload: unknown): Promis args = BuildWorkflowInput.parse(args) } - const result = await tool.execute(args) + const result = await tool.execute(args, context) if (toolName === 'get_blocks_and_tools') { return GetBlocksAndToolsResult.parse(result) diff --git a/apps/sim/lib/copilot/tools/server/user/get-environment-variables.ts b/apps/sim/lib/copilot/tools/server/user/get-environment-variables.ts index e74c9767d7..2ee14aca23 100644 --- a/apps/sim/lib/copilot/tools/server/user/get-environment-variables.ts +++ b/apps/sim/lib/copilot/tools/server/user/get-environment-variables.ts @@ -1,7 +1,7 @@ +import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { getEnvironmentVariableKeys } from '@/lib/environment/utils' import { createLogger } from '@/lib/logs/console/logger' -import { getUserId } from '@/app/api/auth/oauth/utils' interface GetEnvironmentVariablesParams { userId?: string @@ -11,22 +11,41 @@ interface GetEnvironmentVariablesParams { export const getEnvironmentVariablesServerTool: BaseServerTool = { name: 'get_environment_variables', - async execute(params: GetEnvironmentVariablesParams): Promise { + async execute( + params: GetEnvironmentVariablesParams, + context?: { userId: string } + ): Promise { const logger = createLogger('GetEnvironmentVariablesServerTool') - const { userId: directUserId, workflowId } = params || {} - logger.info('Getting environment variables (new runtime)', { - hasUserId: !!directUserId, - hasWorkflowId: !!workflowId, - }) + if (!context?.userId) { + logger.error( + 'Unauthorized attempt to access environment variables - no authenticated user context' + ) + throw new Error('Authentication required') + } + + const authenticatedUserId = context.userId + + if (params?.workflowId) { + const { hasAccess } = await verifyWorkflowAccess(authenticatedUserId, params.workflowId) - const userId = - directUserId || (workflowId ? await getUserId('copilot-env-vars', workflowId) : undefined) - if (!userId) { - logger.warn('No userId could be determined', { directUserId, workflowId }) - throw new Error('Either userId or workflowId is required') + if (!hasAccess) { + const errorMessage = createPermissionError('access environment variables in') + logger.error('Unauthorized attempt to access environment variables', { + workflowId: params.workflowId, + authenticatedUserId, + }) + throw new Error(errorMessage) + } } + const userId = authenticatedUserId + + logger.info('Getting environment variables for authenticated user', { + userId, + hasWorkflowId: !!params?.workflowId, + }) + const result = await getEnvironmentVariableKeys(userId) logger.info('Environment variable keys retrieved', { userId, variableCount: result.count }) return { diff --git a/apps/sim/lib/copilot/tools/server/user/get-oauth-credentials.ts b/apps/sim/lib/copilot/tools/server/user/get-oauth-credentials.ts index 70735093f3..07e0191abb 100644 --- a/apps/sim/lib/copilot/tools/server/user/get-oauth-credentials.ts +++ b/apps/sim/lib/copilot/tools/server/user/get-oauth-credentials.ts @@ -2,10 +2,11 @@ import { db } from '@sim/db' import { account, user } from '@sim/db/schema' import { eq } from 'drizzle-orm' import { jwtDecode } from 'jwt-decode' +import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { createLogger } from '@/lib/logs/console/logger' import { generateRequestId } from '@/lib/utils' -import { getUserId, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' interface GetOAuthCredentialsParams { userId?: string @@ -14,18 +15,35 @@ interface GetOAuthCredentialsParams { export const getOAuthCredentialsServerTool: BaseServerTool = { name: 'get_oauth_credentials', - async execute(params: GetOAuthCredentialsParams): Promise { + async execute(params: GetOAuthCredentialsParams, context?: { userId: string }): Promise { const logger = createLogger('GetOAuthCredentialsServerTool') - const directUserId = params?.userId - let userId = directUserId - if (!userId && params?.workflowId) { - userId = await getUserId('copilot-oauth-creds', params.workflowId) + + if (!context?.userId) { + logger.error( + 'Unauthorized attempt to access OAuth credentials - no authenticated user context' + ) + throw new Error('Authentication required') } - if (!userId || typeof userId !== 'string' || userId.trim().length === 0) { - throw new Error('userId is required') + + const authenticatedUserId = context.userId + + if (params?.workflowId) { + const { hasAccess } = await verifyWorkflowAccess(authenticatedUserId, params.workflowId) + + if (!hasAccess) { + const errorMessage = createPermissionError('access credentials in') + logger.error('Unauthorized attempt to access OAuth credentials', { + workflowId: params.workflowId, + authenticatedUserId, + }) + throw new Error(errorMessage) + } } - logger.info('Fetching OAuth credentials for user', { - hasDirectUserId: !!directUserId, + + const userId = authenticatedUserId + + logger.info('Fetching OAuth credentials for authenticated user', { + userId, hasWorkflowId: !!params?.workflowId, }) const accounts = await db.select().from(account).where(eq(account.userId, userId)) diff --git a/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts b/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts index d277c4ee84..f017a6de09 100644 --- a/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts +++ b/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts @@ -2,10 +2,10 @@ import { db } from '@sim/db' import { environment } from '@sim/db/schema' import { eq } from 'drizzle-orm' import { z } from 'zod' +import { createPermissionError, verifyWorkflowAccess } from '@/lib/copilot/auth/permissions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { createLogger } from '@/lib/logs/console/logger' import { decryptSecret, encryptSecret } from '@/lib/utils' -import { getUserId } from '@/app/api/auth/oauth/utils' interface SetEnvironmentVariablesParams { variables: Record | Array<{ name: string; value: string }> @@ -28,7 +28,6 @@ function normalizeVariables( {} as Record ) } - // Ensure all values are strings return Object.fromEntries( Object.entries(input || {}).map(([k, v]) => [k, String(v ?? '')]) ) as Record @@ -37,19 +36,40 @@ function normalizeVariables( export const setEnvironmentVariablesServerTool: BaseServerTool = { name: 'set_environment_variables', - async execute(params: SetEnvironmentVariablesParams): Promise { + async execute( + params: SetEnvironmentVariablesParams, + context?: { userId: string } + ): Promise { const logger = createLogger('SetEnvironmentVariablesServerTool') + + if (!context?.userId) { + logger.error( + 'Unauthorized attempt to set environment variables - no authenticated user context' + ) + throw new Error('Authentication required') + } + + const authenticatedUserId = context.userId const { variables, workflowId } = params || ({} as SetEnvironmentVariablesParams) + if (workflowId) { + const { hasAccess } = await verifyWorkflowAccess(authenticatedUserId, workflowId) + + if (!hasAccess) { + const errorMessage = createPermissionError('modify environment variables in') + logger.error('Unauthorized attempt to set environment variables', { + workflowId, + authenticatedUserId, + }) + throw new Error(errorMessage) + } + } + + const userId = authenticatedUserId + const normalized = normalizeVariables(variables || {}) const { variables: validatedVariables } = EnvVarSchema.parse({ variables: normalized }) - const userId = await getUserId('copilot-set-env-vars', workflowId) - if (!userId) { - logger.warn('Unauthorized set env vars attempt') - throw new Error('Unauthorized') - } - // Fetch existing const existingData = await db .select() .from(environment) @@ -57,7 +77,6 @@ export const setEnvironmentVariablesServerTool: BaseServerTool) || {} - // Diff and (re)encrypt const toEncrypt: Record = {} const added: string[] = [] const updated: string[] = [] From 20a5156dead707a575cc1b62ccd92f26c8d96995 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 26 Sep 2025 18:03:21 -0700 Subject: [PATCH 2/3] remove extraneous comments, remove the ability to view copilot API keys after creation --- .../api/copilot/api-keys/generate/route.ts | 6 +- apps/sim/app/api/copilot/api-keys/route.ts | 7 +- .../api/copilot/tools/mark-complete/route.ts | 2 - apps/sim/app/api/yaml/autolayout/route.ts | 1 - apps/sim/app/api/yaml/diff/create/route.ts | 1 - apps/sim/app/api/yaml/diff/merge/route.ts | 1 - apps/sim/app/api/yaml/generate/route.ts | 1 - apps/sim/app/api/yaml/health/route.ts | 1 - apps/sim/app/api/yaml/parse/route.ts | 1 - apps/sim/app/api/yaml/to-workflow/route.ts | 1 - .../components/copilot/copilot.tsx | 144 +++++------------- apps/sim/lib/copilot/auth/permissions.ts | 2 - 12 files changed, 51 insertions(+), 117 deletions(-) diff --git a/apps/sim/app/api/copilot/api-keys/generate/route.ts b/apps/sim/app/api/copilot/api-keys/generate/route.ts index 755ee7ee0a..c88f309705 100644 --- a/apps/sim/app/api/copilot/api-keys/generate/route.ts +++ b/apps/sim/app/api/copilot/api-keys/generate/route.ts @@ -15,6 +15,8 @@ export async function POST(req: NextRequest) { // Move environment variable access inside the function const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT + await req.json().catch(() => ({})) + const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/generate`, { method: 'POST', headers: { @@ -31,14 +33,14 @@ export async function POST(req: NextRequest) { ) } - const data = (await res.json().catch(() => null)) as { apiKey?: string } | null + const data = (await res.json().catch(() => null)) as { apiKey?: string; id?: string } | null if (!data?.apiKey) { return NextResponse.json({ error: 'Invalid response from Sim Agent' }, { status: 500 }) } return NextResponse.json( - { success: true, key: { id: 'new', apiKey: data.apiKey } }, + { success: true, key: { id: data?.id || 'new', apiKey: data.apiKey } }, { status: 201 } ) } catch (error) { diff --git a/apps/sim/app/api/copilot/api-keys/route.ts b/apps/sim/app/api/copilot/api-keys/route.ts index 0cbe6cafaf..3de2b0feb0 100644 --- a/apps/sim/app/api/copilot/api-keys/route.ts +++ b/apps/sim/app/api/copilot/api-keys/route.ts @@ -33,7 +33,12 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Invalid response from Sim Agent' }, { status: 500 }) } - const keys = apiKeys + const keys = apiKeys.map((k) => { + const value = typeof k.apiKey === 'string' ? k.apiKey : '' + const last6 = value.slice(-6) + const displayKey = `•••••${last6}` + return { id: k.id, displayKey } + }) return NextResponse.json({ keys }, { status: 200 }) } catch (error) { diff --git a/apps/sim/app/api/copilot/tools/mark-complete/route.ts b/apps/sim/app/api/copilot/tools/mark-complete/route.ts index 17b00b741f..601a652d0a 100644 --- a/apps/sim/app/api/copilot/tools/mark-complete/route.ts +++ b/apps/sim/app/api/copilot/tools/mark-complete/route.ts @@ -13,10 +13,8 @@ import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent/constants' const logger = createLogger('CopilotMarkToolCompleteAPI') -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT -// Schema for mark-complete request const MarkCompleteSchema = z.object({ id: z.string(), name: z.string(), diff --git a/apps/sim/app/api/yaml/autolayout/route.ts b/apps/sim/app/api/yaml/autolayout/route.ts index ecc7b730f0..cef54c59fa 100644 --- a/apps/sim/app/api/yaml/autolayout/route.ts +++ b/apps/sim/app/api/yaml/autolayout/route.ts @@ -18,7 +18,6 @@ import { const logger = createLogger('YamlAutoLayoutAPI') -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT const AutoLayoutRequestSchema = z.object({ diff --git a/apps/sim/app/api/yaml/diff/create/route.ts b/apps/sim/app/api/yaml/diff/create/route.ts index 81148c6520..907dd89540 100644 --- a/apps/sim/app/api/yaml/diff/create/route.ts +++ b/apps/sim/app/api/yaml/diff/create/route.ts @@ -19,7 +19,6 @@ import { const logger = createLogger('YamlDiffCreateAPI') -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT const CreateDiffRequestSchema = z.object({ diff --git a/apps/sim/app/api/yaml/diff/merge/route.ts b/apps/sim/app/api/yaml/diff/merge/route.ts index 4990e46bdb..6d741a4622 100644 --- a/apps/sim/app/api/yaml/diff/merge/route.ts +++ b/apps/sim/app/api/yaml/diff/merge/route.ts @@ -18,7 +18,6 @@ import { const logger = createLogger('YamlDiffMergeAPI') -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT const MergeDiffRequestSchema = z.object({ diff --git a/apps/sim/app/api/yaml/generate/route.ts b/apps/sim/app/api/yaml/generate/route.ts index df84ffb61d..19566707d2 100644 --- a/apps/sim/app/api/yaml/generate/route.ts +++ b/apps/sim/app/api/yaml/generate/route.ts @@ -11,7 +11,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w const logger = createLogger('YamlGenerateAPI') -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT const GenerateRequestSchema = z.object({ diff --git a/apps/sim/app/api/yaml/health/route.ts b/apps/sim/app/api/yaml/health/route.ts index 260565b230..0784df3a08 100644 --- a/apps/sim/app/api/yaml/health/route.ts +++ b/apps/sim/app/api/yaml/health/route.ts @@ -6,7 +6,6 @@ import { generateRequestId } from '@/lib/utils' const logger = createLogger('YamlHealthAPI') -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT export async function GET() { diff --git a/apps/sim/app/api/yaml/parse/route.ts b/apps/sim/app/api/yaml/parse/route.ts index fec4b5deaa..a3d162960d 100644 --- a/apps/sim/app/api/yaml/parse/route.ts +++ b/apps/sim/app/api/yaml/parse/route.ts @@ -11,7 +11,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w const logger = createLogger('YamlParseAPI') -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT const ParseRequestSchema = z.object({ diff --git a/apps/sim/app/api/yaml/to-workflow/route.ts b/apps/sim/app/api/yaml/to-workflow/route.ts index a31a50bca9..262c197583 100644 --- a/apps/sim/app/api/yaml/to-workflow/route.ts +++ b/apps/sim/app/api/yaml/to-workflow/route.ts @@ -11,7 +11,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w const logger = createLogger('YamlToWorkflowAPI') -// Sim Agent API configuration const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT const ConvertRequestSchema = z.object({ diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx index 918a380d07..7c12bd1102 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react' -import { Check, Copy, Eye, EyeOff, Plus, Search } from 'lucide-react' +import { Check, Copy, Plus, Search } from 'lucide-react' import { AlertDialog, AlertDialogAction, @@ -13,10 +13,6 @@ import { Input, Label, Skeleton, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, } from '@/components/ui' import { createLogger } from '@/lib/logs/console/logger' @@ -24,44 +20,36 @@ const logger = createLogger('CopilotSettings') interface CopilotKey { id: string - apiKey: string + displayKey: string } export function Copilot() { const [keys, setKeys] = useState([]) const [isLoading, setIsLoading] = useState(true) - const [visible, setVisible] = useState>({}) const [searchTerm, setSearchTerm] = useState('') // Create flow state const [showNewKeyDialog, setShowNewKeyDialog] = useState(false) - const [newKey, setNewKey] = useState(null) - const [copiedKeyIds, setCopiedKeyIds] = useState>({}) + const [newKey, setNewKey] = useState(null) + const [isCreatingKey] = useState(false) const [newKeyCopySuccess, setNewKeyCopySuccess] = useState(false) // Delete flow state const [deleteKey, setDeleteKey] = useState(null) const [showDeleteDialog, setShowDeleteDialog] = useState(false) - // Filter keys based on search term + // Filter keys based on search term (by masked display value) const filteredKeys = keys.filter((key) => - key.apiKey.toLowerCase().includes(searchTerm.toLowerCase()) + key.displayKey.toLowerCase().includes(searchTerm.toLowerCase()) ) - const maskedValue = useCallback((value: string, show: boolean) => { - if (show) return value - if (!value) return '' - const last6 = value.slice(-6) - return `•••••${last6}` - }, []) - const fetchKeys = useCallback(async () => { try { setIsLoading(true) const res = await fetch('/api/copilot/api-keys') if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`) const data = await res.json() - setKeys(Array.isArray(data.keys) ? data.keys : []) + setKeys(Array.isArray(data.keys) ? (data.keys as CopilotKey[]) : []) } catch (error) { logger.error('Failed to fetch copilot keys', { error }) setKeys([]) @@ -84,10 +72,11 @@ export function Copilot() { } const data = await res.json() // Show the new key dialog with the API key (only shown once) - if (data?.key) { - setNewKey(data.key) + if (data?.key?.apiKey) { + setNewKey(data.key.apiKey) setShowNewKeyDialog(true) } + await fetchKeys() } catch (error) { logger.error('Failed to generate copilot API key', { error }) @@ -117,12 +106,7 @@ export function Copilot() { const onCopy = async (value: string, keyId?: string) => { try { await navigator.clipboard.writeText(value) - if (keyId) { - setCopiedKeyIds((prev) => ({ ...prev, [keyId]: true })) - setTimeout(() => { - setCopiedKeyIds((prev) => ({ ...prev, [keyId]: false })) - }, 1500) - } else { + if (!keyId) { setNewKeyCopySuccess(true) setTimeout(() => setNewKeyCopySuccess(false), 1500) } @@ -166,77 +150,32 @@ export function Copilot() { ) : (
- {filteredKeys.map((k) => { - const isVisible = !!visible[k.id] - const value = maskedValue(k.apiKey, isVisible) - return ( -
- -
-
-
- {value} -
-
- - - - - - {isVisible ? 'Hide' : 'Reveal'} - - - - - - - - - Copy - - -
+ {filteredKeys.map((k) => ( +
+ +
+
+
+ {k.displayKey}
- -
+ +
- ) - })} +
+ ))} {/* Show message when search has no results but there are keys */} {searchTerm.trim() && filteredKeys.length === 0 && keys.length > 0 && (
@@ -265,7 +204,7 @@ export function Copilot() { disabled={isLoading} > - Generate Key + Create Key )} @@ -285,24 +224,23 @@ export function Copilot() { > - New Copilot API Key + Your API key has been created - Copy it now and store it securely. + This is the only time you will see your API key.{' '} + Copy it now and store it securely. {newKey && (
- - {newKey.apiKey} - + {newKey}