1010 * never display it again — if lost, revoke it and issue a new one.
1111 */
1212
13- import { useState } from 'react' ;
14- import { AlertTriangle , Check , Copy , KeyRound , Loader2 , ShieldCheck , X } from 'lucide-react' ;
13+ import { useEffect , useState } from 'react' ;
14+ import { AlertTriangle , Check , Copy , KeyRound , Loader2 , Lock , ShieldCheck , X } from 'lucide-react' ;
1515import { Button , Card , CardContent , CardDescription , CardHeader , CardTitle } from '@mcpmux/ui' ;
1616import { registerApiKeyClient , type RegisteredApiKeyClient } from '@/lib/api/gateway' ;
17+ import { listSpaces , type Space } from '@/lib/api/spaces' ;
1718
1819interface RegisterApiKeyClientModalProps {
1920 onClose : ( ) => void ;
@@ -26,11 +27,23 @@ export function RegisterApiKeyClientModal({
2627 onRegistered,
2728} : RegisterApiKeyClientModalProps ) {
2829 const [ name , setName ] = useState ( '' ) ;
30+ const [ lockedSpaceId , setLockedSpaceId ] = useState ( '' ) ;
31+ const [ spaces , setSpaces ] = useState < Space [ ] > ( [ ] ) ;
2932 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
3033 const [ error , setError ] = useState < string | null > ( null ) ;
3134 const [ result , setResult ] = useState < RegisteredApiKeyClient | null > ( null ) ;
3235 const [ copied , setCopied ] = useState ( false ) ;
3336
37+ useEffect ( ( ) => {
38+ listSpaces ( )
39+ . then ( setSpaces )
40+ . catch ( ( ) => setSpaces ( [ ] ) ) ;
41+ } , [ ] ) ;
42+
43+ const lockedSpaceName = result ?. lockedSpaceId
44+ ? ( spaces . find ( ( s ) => s . id === result . lockedSpaceId ) ?. name ?? 'a Space' )
45+ : null ;
46+
3447 const handleGenerate = async ( ) => {
3548 const trimmed = name . trim ( ) ;
3649 if ( ! trimmed ) {
@@ -40,7 +53,7 @@ export function RegisterApiKeyClientModal({
4053 setIsSubmitting ( true ) ;
4154 setError ( null ) ;
4255 try {
43- const client = await registerApiKeyClient ( trimmed ) ;
56+ const client = await registerApiKeyClient ( trimmed , lockedSpaceId || null ) ;
4457 setResult ( client ) ;
4558 } catch ( e ) {
4659 setError ( e instanceof Error ? e . message : String ( e ) ) ;
@@ -130,6 +143,13 @@ export function RegisterApiKeyClientModal({
130143 < code className = "block break-all font-mono text-xs text-[rgb(var(--text))]" >
131144 Authorization: Bearer { result . keyPrefix } …
132145 </ code >
146+ { lockedSpaceName && (
147+ < p className = "mt-2 flex items-center gap-1.5 text-xs text-[rgb(var(--muted))]" >
148+ < Lock className = "h-3.5 w-3.5" />
149+ Locked to < span className = "font-medium" > { lockedSpaceName } </ span > — this key can
150+ only ever reach that Space.
151+ </ p >
152+ ) }
133153 </ div >
134154
135155 < div className = "flex justify-end" >
@@ -159,6 +179,30 @@ export function RegisterApiKeyClientModal({
159179 />
160180 </ div >
161181
182+ < div >
183+ < label htmlFor = "api-key-lock-space" className = "mb-1.5 block text-sm font-medium" >
184+ Lock to a Space < span className = "text-[rgb(var(--muted))]" > (optional)</ span >
185+ </ label >
186+ < select
187+ id = "api-key-lock-space"
188+ data-testid = "register-api-key-lock-space"
189+ value = { lockedSpaceId }
190+ onChange = { ( e ) => setLockedSpaceId ( e . target . value ) }
191+ className = "w-full rounded-xl border border-[rgb(var(--border))] bg-[rgb(var(--surface))] px-3.5 py-2.5 text-sm transition-all focus:border-[rgb(var(--accent))] focus:outline-none focus:ring-2 focus:ring-[rgb(var(--accent))]/40"
192+ >
193+ < option value = "" > No lock — route by mapping (any Space)</ option >
194+ { spaces . map ( ( s ) => (
195+ < option key = { s . id } value = { s . id } >
196+ { s . name }
197+ </ option >
198+ ) ) }
199+ </ select >
200+ < p className = "mt-1.5 text-xs text-[rgb(var(--muted))]" >
201+ Locking confines this client to one Space — a leaked key can never reach the
202+ others. Leave unlocked to route it later from the Workspaces tab.
203+ </ p >
204+ </ div >
205+
162206 < div className = "flex items-start gap-3 rounded-xl border border-[rgb(var(--border-subtle))] bg-[rgb(var(--surface))] p-3.5" >
163207 < ShieldCheck className = "mt-0.5 h-5 w-5 flex-shrink-0 text-[rgb(var(--accent))]" />
164208 < p className = "text-xs text-[rgb(var(--muted))]" >
0 commit comments