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";
2−
3−import { useMemo, useState } from "react";
4−import { useQuery } from "convex/react";
5−import { Check, Copy } from "lucide-react";
6−import type { Id } from "@executor/database/convex/_generated/dataModel";
7−import { Button } from "@/components/ui/button";
8−import { Input } from "@/components/ui/input";
9−import { Label } from "@/components/ui/label";
10−import { getAddMcpInstallConfig } from "@/components/tools/install-configs";
11−import { convexApi } from "@/lib/convex-api";
1+"use client"
2+
3+import { useMemo, useState } from "react"
4+import { useQuery } from "convex/react"
5+import { Check, Copy, Eye, EyeOff } from "lucide-react"
6+import type { Id } from "@executor/database/convex/_generated/dataModel"
7+import { Button } from "@/components/ui/button"
8+import { Input } from "@/components/ui/input"
9+import { Label } from "@/components/ui/label"
10+import { getAddMcpInstallConfig } from "@/components/tools/install-configs"
11+import { convexApi } from "@/lib/convex-api"
1212
1313 function inferServerName(workspaceId?: string): string {
14− if (!workspaceId) return "executor";
15− return `executor-${workspaceId.slice(0, 8).toLowerCase()}`;
14+ if (!workspaceId) return "executor"
15+ return `executor-${workspaceId.slice(0, 8).toLowerCase()}`
1616 }
1717
1818 function isAnonymousSessionId(sessionId?: string): boolean {
19− if (!sessionId) return false;
20− return sessionId.startsWith("anon_session_") || sessionId.startsWith("mcp_");
19+ if (!sessionId) return false
20+ return sessionId.startsWith("anon_session_") || sessionId.startsWith("mcp_")
2121 }
2222
2323 function isWorkosSessionId(sessionId?: string): boolean {
24− if (!sessionId) return false;
25− return sessionId.startsWith("workos_");
24+ if (!sessionId) return false
25+ return sessionId.startsWith("workos_")
2626 }
2727
2828 function 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
3030 if (explicit && explicit.trim().length > 0) {
31− return explicit.trim().replace(/\/$/, "");
31+ return explicit.trim().replace(/\/$/, "")
3232 }
3333
34− const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
34+ const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
3535 if (convexUrl) {
3636 try {
37− const parsed = new URL(convexUrl);
37+ const parsed = new URL(convexUrl)
3838 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")
4040 }
41− return parsed.origin;
41+ return parsed.origin
4242 } catch {
4343 // Fallback to web origin below.
4444 }
4545 }
4646
47− return windowOrigin;
47+ return windowOrigin
4848 }
4949
5050 export function McpSetupCard({
5151 workspaceId,
5252 sessionId,
5353 }: {
54− workspaceId?: Id<"workspaces">;
55− sessionId?: string;
54+ workspaceId?: Id<"workspaces">
55+ sessionId?: string
5656 }) {
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)
5860
5961 const origin = useMemo(() => {
60− if (typeof window === "undefined") return "";
61− return resolveMcpOrigin(window.location.origin);
62− }, []);
62+ if (typeof window === "undefined") return ""
63+ return resolveMcpOrigin(window.location.origin)
64+ }, [])
6365
6466 const mcpUrl = useMemo(() => {
65− const isAnonymousSession = isAnonymousSessionId(sessionId);
66− const mcpPath = isAnonymousSession ? "/mcp/anonymous" : "/mcp";
67− const base = origin ? new URL(mcpPath, origin) : new URL(`http://localhost${mcpPath}`);
68− if (workspaceId) base.searchParams.set("workspaceId", workspaceId);
67+ const isAnonymousSession = isAnonymousSessionId(sessionId)
68+ const mcpPath = isAnonymousSession ? "/mcp/anonymous" : "/mcp"
69+ const base = origin ? new URL(mcpPath, origin) : new URL(`http://localhost${mcpPath}`)
70+ if (workspaceId) base.searchParams.set("workspaceId", workspaceId)
6971 if (!isAnonymousSession && sessionId && !isWorkosSessionId(sessionId)) {
70− base.searchParams.set("sessionId", sessionId);
72+ base.searchParams.set("sessionId", sessionId)
7173 }
7274 if (!origin) {
73− return `${base.pathname}${base.search}`;
75+ return `${base.pathname}${base.search}`
7476 }
75− return base.toString();
76− }, [origin, workspaceId, sessionId]);
77+ return base.toString()
78+ }, [origin, workspaceId, sessionId])
7779
78− const isAnonymousSession = isAnonymousSessionId(sessionId);
80+ const isAnonymousSession = isAnonymousSessionId(sessionId)
7981
8082 const anonymousMcpApiKey = useQuery(
8183 convexApi.workspace.getMcpApiKey,
8284 isAnonymousSession && workspaceId
8385 ? {
8486 workspaceId,
85− sessionId
87+ sessionId,
8688 }
8789 : "skip",
88− );
90+ )
8991
90−
91− const apiKeyValue = anonymousMcpApiKey?.enabled
92− ? anonymousMcpApiKey.apiKey
93− : null;
92+ const apiKeyValue = anonymousMcpApiKey?.enabled ? anonymousMcpApiKey.apiKey : null
9493 const apiKeyError = isAnonymousSession
9594 ? anonymousMcpApiKey?.enabled === false
9695 ? anonymousMcpApiKey.error
9796 : workspaceId
9897 ? null
9998 : "Workspace is required to issue an MCP API key"
100− : null;
99+ : null
101100
102− const serverName = inferServerName(workspaceId);
101+ const serverName = inferServerName(workspaceId)
103102 const installConfig = getAddMcpInstallConfig(
104103 mcpUrl,
105104 serverName,
@@ -108,17 +107,29 @@ export function McpSetupCard({ · 2 unmodified lines
108107 apiKey: apiKeyValue ?? "your-api-key-here",
109108 }
110109 : 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"
112125
113126 const copyText = async (key: string, text: string) => {
114− await navigator.clipboard.writeText(text);
115− setCopiedKey(key);
116− setTimeout(() => setCopiedKey((current) => (current === key ? null : current)), 1500);
117− };
127+ await navigator.clipboard.writeText(text)
128+ setCopiedKey(key)
129+ setTimeout(() => setCopiedKey((current) => (current === key ? null : current)), 1500)
130+ }
118131
119− const codeLanguage = installConfig.type === "command"
120− ? "bash"
121− : installConfig.type;
132+ const codeLanguage = installConfig.type === "command" ? "bash" : installConfig.type
122133
123134 return (
124135 <div className="space-y-5">
@@ -140,18 +151,34 @@ export function McpSetupCard({ · 15 unmodified lines
140151 {isAnonymousSession && (
141152 <div className="mt-2 space-y-1.5">
142153 <div className="flex items-center gap-2">
143− <Input
144− value={apiKeyValue ?? "Loading API key..."}
145− readOnly
146− className="h-8 text-xs font-mono bg-background"
147− />
154+ <div className="relative flex-1">
155+ <Input
156+ value={apiKeyInputValue}
157+ type={apiKeyInputType}
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>
148175 <Button
149176 type="button"
150177 size="icon"
151178 variant="outline"
152179 className="h-8 w-8"
153180 disabled={!apiKeyValue}
154− onClick={() => apiKeyValue ? void copyText("api-key", apiKeyValue) : undefined}
181+ onClick={() => void (apiKeyValue ? copyText("api-key", apiKeyValue) : undefined)}
155182 >
156183 {copiedKey === "api-key" ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
157184 </Button>
@@ -164,8 +191,26 @@ export function McpSetupCard({ · 6 unmodified lines
164191 <div className="space-y-2">
165192 <p className="text-[11px] text-muted-foreground">{installConfig.description}</p>
166193 <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}
167212 <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>
169214 </pre>
170215 <Button
171216 type="button"
@@ -178,7 +223,6 @@ export function McpSetupCard({ · 6 unmodified lines
178223 </Button>
179224 </div>
180225 </div>
181−
182226 </div>
183− );
227+ )
184228 }