History

hide api keys

Rhys Sullivan committed 9 months ago

Commit 168fb1d1 parent c458d211 file · +108 −64Browse files

Unified · Split

executor/apps/web/src/components/tools/setup-card.tsx+108−64
@@ -1,105 +1,104 @@
1"use client";1"use client"
2 2
3import { useMemo, useState } from "react";3import { useMemo, useState } from "react"
4import { useQuery } from "convex/react";4import { useQuery } from "convex/react"
5import { Check, Copy } from "lucide-react";5import { Check, Copy, Eye, EyeOff } from "lucide-react"
6import type { Id } from "@executor/database/convex/_generated/dataModel";6import type { Id } from "@executor/database/convex/_generated/dataModel"
7import { Button } from "@/components/ui/button";7import { Button } from "@/components/ui/button"
8import { Input } from "@/components/ui/input";8import { Input } from "@/components/ui/input"
9import { Label } from "@/components/ui/label";9import { Label } from "@/components/ui/label"
10import { getAddMcpInstallConfig } from "@/components/tools/install-configs";10import { getAddMcpInstallConfig } from "@/components/tools/install-configs"
11import { convexApi } from "@/lib/convex-api";11import { convexApi } from "@/lib/convex-api"
12 12
13function inferServerName(workspaceId?: string): string {13function inferServerName(workspaceId?: string): string {
14 if (!workspaceId) return "executor";14 if (!workspaceId) return "executor"
15 return `executor-${workspaceId.slice(0, 8).toLowerCase()}`;15 return `executor-${workspaceId.slice(0, 8).toLowerCase()}`
16}16}
17 17
18function isAnonymousSessionId(sessionId?: string): boolean {18function isAnonymousSessionId(sessionId?: string): boolean {
19 if (!sessionId) return false;19 if (!sessionId) return false
20 return sessionId.startsWith("anon_session_") || sessionId.startsWith("mcp_");20 return sessionId.startsWith("anon_session_") || sessionId.startsWith("mcp_")
21}21}
22 22
23function isWorkosSessionId(sessionId?: string): boolean {23function isWorkosSessionId(sessionId?: string): boolean {
24 if (!sessionId) return false;24 if (!sessionId) return false
25 return sessionId.startsWith("workos_");25 return sessionId.startsWith("workos_")
26}26}
27 27
28function resolveMcpOrigin(windowOrigin: string): string {28function resolveMcpOrigin(windowOrigin: string): string {
29 const explicit = process.env.NEXT_PUBLIC_EXECUTOR_HTTP_URL ?? process.env.NEXT_PUBLIC_CONVEX_SITE_URL;29 const explicit = process.env.NEXT_PUBLIC_EXECUTOR_HTTP_URL ?? process.env.NEXT_PUBLIC_CONVEX_SITE_URL
30 if (explicit && explicit.trim().length > 0) {30 if (explicit && explicit.trim().length > 0) {
31 return explicit.trim().replace(/\/$/, "");31 return explicit.trim().replace(/\/$/, "")
32 }32 }
33 33
34 const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;34 const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
35 if (convexUrl) {35 if (convexUrl) {
36 try {36 try {
37 const parsed = new URL(convexUrl);37 const parsed = new URL(convexUrl)
38 if (parsed.hostname.endsWith(".convex.cloud")) {38 if (parsed.hostname.endsWith(".convex.cloud")) {
39 parsed.hostname = parsed.hostname.replace(/\.convex\.cloud$/, ".convex.site");39 parsed.hostname = parsed.hostname.replace(/\.convex\.cloud$/, ".convex.site")
40 }40 }
41 return parsed.origin;41 return parsed.origin
42 } catch {42 } catch {
43 // Fallback to web origin below.43 // Fallback to web origin below.
44 }44 }
45 }45 }
46 46
47 return windowOrigin;47 return windowOrigin
48}48}
49 49
50export function McpSetupCard({50export function McpSetupCard({
51 workspaceId,51 workspaceId,
52 sessionId,52 sessionId,
53}: {53}: {
54 workspaceId?: Id<"workspaces">;54 workspaceId?: Id<"workspaces">
55 sessionId?: string;55 sessionId?: string
56}) {56}) {
57 const [copiedKey, setCopiedKey] = useState<string | null>(null);57 const [copiedKey, setCopiedKey] = useState<string | null>(null)
58 const [showApiKey, setShowApiKey] = useState(false)
59 const [showInstallApiKey, setShowInstallApiKey] = useState(false)
58 60
59 const origin = useMemo(() => {61 const origin = useMemo(() => {
60 if (typeof window === "undefined") return "";62 if (typeof window === "undefined") return ""
61 return resolveMcpOrigin(window.location.origin);63 return resolveMcpOrigin(window.location.origin)
62 }, []);64 }, [])
63 65
64 const mcpUrl = useMemo(() => {66 const mcpUrl = useMemo(() => {
65 const isAnonymousSession = isAnonymousSessionId(sessionId);67 const isAnonymousSession = isAnonymousSessionId(sessionId)
66 const mcpPath = isAnonymousSession ? "/mcp/anonymous" : "/mcp";68 const mcpPath = isAnonymousSession ? "/mcp/anonymous" : "/mcp"
67 const base = origin ? new URL(mcpPath, origin) : new URL(`http://localhost${mcpPath}`);69 const base = origin ? new URL(mcpPath, origin) : new URL(`http://localhost${mcpPath}`)
68 if (workspaceId) base.searchParams.set("workspaceId", workspaceId);70 if (workspaceId) base.searchParams.set("workspaceId", workspaceId)
69 if (!isAnonymousSession && sessionId && !isWorkosSessionId(sessionId)) {71 if (!isAnonymousSession && sessionId && !isWorkosSessionId(sessionId)) {
70 base.searchParams.set("sessionId", sessionId);72 base.searchParams.set("sessionId", sessionId)
71 }73 }
72 if (!origin) {74 if (!origin) {
73 return `${base.pathname}${base.search}`;75 return `${base.pathname}${base.search}`
74 }76 }
75 return base.toString();77 return base.toString()
76 }, [origin, workspaceId, sessionId]);78 }, [origin, workspaceId, sessionId])
77 79
78 const isAnonymousSession = isAnonymousSessionId(sessionId);80 const isAnonymousSession = isAnonymousSessionId(sessionId)
79 81
80 const anonymousMcpApiKey = useQuery(82 const anonymousMcpApiKey = useQuery(
81 convexApi.workspace.getMcpApiKey,83 convexApi.workspace.getMcpApiKey,
82 isAnonymousSession && workspaceId84 isAnonymousSession && workspaceId
83 ? {85 ? {
84 workspaceId,86 workspaceId,
85 sessionId87 sessionId,
86 }88 }
87 : "skip",89 : "skip",
88 );90 )
89 91
90 92 const apiKeyValue = anonymousMcpApiKey?.enabled ? anonymousMcpApiKey.apiKey : null
91 const apiKeyValue = anonymousMcpApiKey?.enabled
92 ? anonymousMcpApiKey.apiKey
93 : null;
94 const apiKeyError = isAnonymousSession93 const apiKeyError = isAnonymousSession
95 ? anonymousMcpApiKey?.enabled === false94 ? anonymousMcpApiKey?.enabled === false
96 ? anonymousMcpApiKey.error95 ? anonymousMcpApiKey.error
97 : workspaceId96 : workspaceId
98 ? null97 ? null
99 : "Workspace is required to issue an MCP API key"98 : "Workspace is required to issue an MCP API key"
100 : null;99 : null
101 100
102 const serverName = inferServerName(workspaceId);101 const serverName = inferServerName(workspaceId)
103 const installConfig = getAddMcpInstallConfig(102 const installConfig = getAddMcpInstallConfig(
104 mcpUrl,103 mcpUrl,
105 serverName,104 serverName,
@@ -108,17 +107,29 @@ export function McpSetupCard({ · 2 unmodified lines
108 apiKey: apiKeyValue ?? "your-api-key-here",107 apiKey: apiKeyValue ?? "your-api-key-here",
109 }108 }
110 : undefined,109 : undefined,
111 );110 )
111
112 const installCommand = useMemo(() => {
113 if (!isAnonymousSession || showInstallApiKey) {
114 return installConfig.content
115 }
116
117 return installConfig.content.replace(
118 / --header "x-api-key: [^"]+"/g,
119 ' --header "x-api-key: ********"',
120 )
121 }, [installConfig.content, isAnonymousSession, showInstallApiKey])
122
123 const apiKeyInputValue = apiKeyValue ?? "Loading API key..."
124 const apiKeyInputType = apiKeyValue ? (showApiKey ? "text" : "password") : "text"
112 125
113 const copyText = async (key: string, text: string) => {126 const copyText = async (key: string, text: string) => {
114 await navigator.clipboard.writeText(text);127 await navigator.clipboard.writeText(text)
115 setCopiedKey(key);128 setCopiedKey(key)
116 setTimeout(() => setCopiedKey((current) => (current === key ? null : current)), 1500);129 setTimeout(() => setCopiedKey((current) => (current === key ? null : current)), 1500)
117 };130 }
118 131
119 const codeLanguage = installConfig.type === "command"132 const codeLanguage = installConfig.type === "command" ? "bash" : installConfig.type
120 ? "bash"
121 : installConfig.type;
122 133
123 return (134 return (
124 <div className="space-y-5">135 <div className="space-y-5">
@@ -140,18 +151,34 @@ export function McpSetupCard({ · 15 unmodified lines
140 {isAnonymousSession && (151 {isAnonymousSession && (
141 <div className="mt-2 space-y-1.5">152 <div className="mt-2 space-y-1.5">
142 <div className="flex items-center gap-2">153 <div className="flex items-center gap-2">
143 <Input154 <div className="relative flex-1">
144 value={apiKeyValue ?? "Loading API key..."}155 <Input
145 readOnly156 value={apiKeyInputValue}
146 className="h-8 text-xs font-mono bg-background"157 type={apiKeyInputType}
147 />158 readOnly
159 className="h-8 text-xs font-mono bg-background pr-8"
160 autoComplete="off"
161 inputMode="text"
162 />
163 <Button
164 type="button"
165 size="icon"
166 variant="ghost"
167 className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 text-muted-foreground hover:text-foreground"
168 onClick={() => void setShowApiKey((next) => !next)}
169 disabled={!apiKeyValue}
170 aria-label={showApiKey ? "Hide API key" : "Show API key"}
171 >
172 {showApiKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
173 </Button>
174 </div>
148 <Button175 <Button
149 type="button"176 type="button"
150 size="icon"177 size="icon"
151 variant="outline"178 variant="outline"
152 className="h-8 w-8"179 className="h-8 w-8"
153 disabled={!apiKeyValue}180 disabled={!apiKeyValue}
154 onClick={() => apiKeyValue ? void copyText("api-key", apiKeyValue) : undefined}181 onClick={() => void (apiKeyValue ? copyText("api-key", apiKeyValue) : undefined)}
155 >182 >
156 {copiedKey === "api-key" ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}183 {copiedKey === "api-key" ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
157 </Button>184 </Button>
@@ -164,8 +191,26 @@ export function McpSetupCard({ · 6 unmodified lines
164 <div className="space-y-2">191 <div className="space-y-2">
165 <p className="text-[11px] text-muted-foreground">{installConfig.description}</p>192 <p className="text-[11px] text-muted-foreground">{installConfig.description}</p>
166 <div className="rounded-md border border-border bg-muted/30 p-3 space-y-2">193 <div className="rounded-md border border-border bg-muted/30 p-3 space-y-2">
194 {isAnonymousSession && apiKeyValue ? (
195 <div className="flex justify-end">
196 <Button
197 type="button"
198 size="icon"
199 variant="outline"
200 className="h-7 w-7"
201 onClick={() => void setShowInstallApiKey((next) => !next)}
202 aria-label={
203 showInstallApiKey
204 ? "Hide API key in install command"
205 : "Reveal API key in install command"
206 }
207 >
208 {showInstallApiKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
209 </Button>
210 </div>
211 ) : null}
167 <pre className="text-[11px] font-mono whitespace-pre-wrap break-all text-foreground">212 <pre className="text-[11px] font-mono whitespace-pre-wrap break-all text-foreground">
168 <code className={`language-${codeLanguage}`}>{installConfig.content}</code>213 <code className={`language-${codeLanguage}`}>{installCommand}</code>
169 </pre>214 </pre>
170 <Button215 <Button
171 type="button"216 type="button"
@@ -178,7 +223,6 @@ export function McpSetupCard({ · 6 unmodified lines
178 </Button>223 </Button>
179 </div>224 </div>
180 </div>225 </div>
181
182 </div>226 </div>
183 );227 )
184}228}