Skip to content

Commit 9ac1227

Browse files
committed
Implement API key generation and display for agents
- Add API key utility functions (generateApiKey, extractAgentIdFromApiKey) - Auto-generate API keys on agent creation (format: pub_key_{agentId}) - Auto-generate missing API keys on agent update (backward compatibility) - Update type definitions to include public_api_key field - Add frontend component to display and copy API keys - Integrate API key section into agent edit page
1 parent 78a98a5 commit 9ac1227

7 files changed

Lines changed: 186 additions & 3 deletions

File tree

frontend/app/dashboard/agents/[id]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { AgentAIBehavior } from '@/components/agents/agent-ai-behavior'
2020
import { AgentModelSettings } from '@/components/agents/agent-model-settings'
2121
import { AgentVoiceSettings } from '@/components/agents/agent-voice-settings'
2222
import { AgentKnowledgeBaseSection } from '@/components/agents/agent-knowledge-base-section'
23+
import { AgentApiKeySection } from '@/components/agents/agent-api-key-section'
2324

2425

2526
export default function EditAgentPage({ params }: { params: Promise<{ id: string }> }) {
@@ -164,6 +165,7 @@ export default function EditAgentPage({ params }: { params: Promise<{ id: string
164165
<AgentAIBehavior form={form} />
165166
<AgentModelSettings form={form} />
166167
<AgentVoiceSettings form={form} />
168+
<AgentApiKeySection agentId={id} apiKey={agent?.public_api_key} />
167169
<AgentKnowledgeBaseSection agentId={id} />
168170
</form>
169171
</Form>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5+
import { Button } from '@/components/ui/button'
6+
import { Input } from '@/components/ui/input'
7+
import { Eye, EyeOff, Copy, Check } from 'lucide-react'
8+
import { toast } from 'sonner'
9+
10+
interface AgentApiKeySectionProps {
11+
agentId: string
12+
apiKey: string | null | undefined
13+
}
14+
15+
export function AgentApiKeySection({ agentId, apiKey }: AgentApiKeySectionProps) {
16+
const [isVisible, setIsVisible] = useState(false)
17+
const [copied, setCopied] = useState(false)
18+
19+
const handleCopy = async () => {
20+
if (!apiKey) {
21+
toast.error('API key not available')
22+
return
23+
}
24+
25+
try {
26+
await navigator.clipboard.writeText(apiKey)
27+
setCopied(true)
28+
toast.success('API key copied to clipboard')
29+
setTimeout(() => setCopied(false), 2000)
30+
} catch (error) {
31+
toast.error('Failed to copy API key')
32+
}
33+
}
34+
35+
const displayKey = apiKey || 'Not generated yet'
36+
37+
return (
38+
<Card>
39+
<CardHeader>
40+
<CardTitle>API Key</CardTitle>
41+
<CardDescription>
42+
Use this API key to authenticate widget requests. Keep it secure and never share it publicly.
43+
</CardDescription>
44+
</CardHeader>
45+
<CardContent className="space-y-4">
46+
<div className="flex gap-2">
47+
<div className="relative flex-1">
48+
<Input
49+
type={isVisible ? 'text' : 'password'}
50+
value={displayKey}
51+
readOnly
52+
className="font-mono text-sm pr-20"
53+
placeholder="API key will appear here"
54+
/>
55+
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
56+
<Button
57+
type="button"
58+
variant="ghost"
59+
size="icon"
60+
className="h-8 w-8"
61+
onClick={() => setIsVisible(!isVisible)}
62+
disabled={!apiKey}
63+
>
64+
{isVisible ? (
65+
<EyeOff className="h-4 w-4" />
66+
) : (
67+
<Eye className="h-4 w-4" />
68+
)}
69+
</Button>
70+
<Button
71+
type="button"
72+
variant="ghost"
73+
size="icon"
74+
className="h-8 w-8"
75+
onClick={handleCopy}
76+
disabled={!apiKey}
77+
>
78+
{copied ? (
79+
<Check className="h-4 w-4 text-green-600" />
80+
) : (
81+
<Copy className="h-4 w-4" />
82+
)}
83+
</Button>
84+
</div>
85+
</div>
86+
</div>
87+
88+
{!apiKey && (
89+
<div className="rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 p-3">
90+
<p className="text-sm text-yellow-800 dark:text-yellow-200">
91+
API key will be generated automatically when you save changes to this agent.
92+
</p>
93+
</div>
94+
)}
95+
96+
{apiKey && (
97+
<div className="rounded-md bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-3">
98+
<p className="text-sm text-blue-800 dark:text-blue-200">
99+
<strong>Security Note:</strong> This API key provides access to your agent. Keep it private and only use it in your widget implementation.
100+
</p>
101+
</div>
102+
)}
103+
</CardContent>
104+
</Card>
105+
)
106+
}
107+

services/agent/src/routes/agents.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
*/
44

55
import express from 'express'
6+
import { randomUUID } from 'crypto'
67
import { supabase } from '../config/database.js'
78
import { authenticate, requireCompany, AuthenticatedRequest } from '../middleware/auth.js'
89
import { CreateAgentSchema, UpdateAgentSchema } from '../schemas/agent.js'
910
import { handleError, notFound, forbidden, badRequest } from '../utils/errors.js'
1011
import { createLogger } from '@syntera/shared/logger/index.js'
1112
import { invalidateAgentConfig, getAgentConfig } from '../utils/agent-cache.js'
13+
import { generateApiKey } from '../utils/api-key.js'
1214

1315
const logger = createLogger('agent-service')
1416
const router = express.Router()
@@ -95,10 +97,15 @@ router.post(
9597
const companyId = req.user!.company_id!
9698
const agentData = validationResult.data
9799

98-
// Insert agent
100+
// Generate agent ID and API key before insert
101+
const agentId = randomUUID()
102+
const publicApiKey = generateApiKey(agentId)
103+
104+
// Insert agent with generated ID and API key
99105
const { data: agent, error } = await supabase
100106
.from('agent_configs')
101107
.insert({
108+
id: agentId,
102109
company_id: companyId,
103110
name: agentData.name,
104111
description: agentData.description || null,
@@ -109,6 +116,7 @@ router.post(
109116
enabled: agentData.enabled,
110117
avatar_url: agentData.avatar_url || null,
111118
voice_settings: agentData.voice_settings || {},
119+
public_api_key: publicApiKey,
112120
})
113121
.select()
114122
.single()
@@ -147,7 +155,7 @@ router.patch(
147155
// Check if agent exists and belongs to company
148156
const { data: existingAgent, error: fetchError } = await supabase
149157
.from('agent_configs')
150-
.select('id, company_id')
158+
.select('id, company_id, public_api_key')
151159
.eq('id', id)
152160
.single()
153161

@@ -159,8 +167,25 @@ router.patch(
159167
return forbidden(res, 'Agent does not belong to your company')
160168
}
161169

162-
// Update agent
170+
// Prepare update data
163171
const updateData = validationResult.data
172+
173+
// Generate API key if missing (backward compatibility)
174+
if (!existingAgent.public_api_key) {
175+
try {
176+
const publicApiKey = generateApiKey(id)
177+
updateData.public_api_key = publicApiKey
178+
logger.info('Generated missing API key for agent', { agentId: id })
179+
} catch (keyError) {
180+
logger.warn('Failed to generate API key for agent', {
181+
agentId: id,
182+
error: keyError instanceof Error ? keyError.message : String(keyError)
183+
})
184+
// Continue without API key - can be generated later
185+
}
186+
}
187+
188+
// Update agent
164189
const { data: agent, error } = await supabase
165190
.from('agent_configs')
166191
.update({

services/agent/src/types/agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface AgentConfig {
1414
max_tokens: number
1515
enabled: boolean
1616
voice_settings?: Record<string, unknown>
17+
public_api_key?: string | null
1718
created_at?: string
1819
updated_at?: string
1920
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* API Key Utility Functions
3+
*
4+
* Handles generation and validation of public API keys for agent widget access.
5+
* API keys follow the format: `pub_key_{agentId}` where agentId is a UUID.
6+
*/
7+
8+
/**
9+
* Generate public API key for an agent
10+
* Format: pub_key_{agentId}
11+
*
12+
* @param agentId - UUID of the agent
13+
* @returns API key string in format `pub_key_{agentId}`
14+
* @throws Error if agentId format is invalid
15+
*/
16+
export function generateApiKey(agentId: string): string {
17+
// Validate UUID format
18+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
19+
if (!agentId || !uuidRegex.test(agentId)) {
20+
throw new Error(`Invalid agent ID format for API key generation: ${agentId}`)
21+
}
22+
return `pub_key_${agentId}`
23+
}
24+
25+
/**
26+
* Extract agent ID from API key
27+
*
28+
* @param apiKey - API key string (format: pub_key_{agentId})
29+
* @returns Agent ID if format is valid, null otherwise
30+
*/
31+
export function extractAgentIdFromApiKey(apiKey: string): string | null {
32+
if (!apiKey || !apiKey.startsWith('pub_key_')) {
33+
return null
34+
}
35+
36+
const agentId = apiKey.substring(8) // Remove 'pub_key_' prefix
37+
38+
// Validate UUID format
39+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
40+
if (uuidRegex.test(agentId)) {
41+
return agentId
42+
}
43+
44+
return null
45+
}
46+

shared/src/schemas/agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const AgentResponseSchema = z.object({
4545
enabled: z.boolean(),
4646
voice_settings: z.record(z.string(), z.unknown()).nullable(),
4747
avatar_url: z.string().url().nullable().optional(),
48+
public_api_key: z.string().nullable().optional(),
4849
created_at: z.string(),
4950
updated_at: z.string(),
5051
})

shared/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface AgentConfig {
4040
max_tokens: number
4141
enabled: boolean
4242
voice_settings?: VoiceSettings | null
43+
public_api_key?: string | null
4344
created_at: string
4445
updated_at: string
4546
}

0 commit comments

Comments
 (0)