Unified · Split
| @@ -8,22 +8,18 @@ export function getAddMcpInstallConfig( · 7 unmodified lines | ||
| 8 | 8 | mcpUrl: string, |
| 9 | 9 | serverName: string, |
| 10 | 10 | auth?: { |
| 11 | − apiKeyEnvName?: string; | |
| 11 | + apiKey?: string; | |
| 12 | 12 | }, |
| 13 | 13 | ): McpInstallConfig { |
| 14 | − const apiKeyEnvName = auth?.apiKeyEnvName?.trim() || "API_KEY"; | |
| 15 | 14 | const headerArg = auth |
| 16 | − ? ` --header "x-api-key: $${apiKeyEnvName}"` | |
| 17 | − : ""; | |
| 18 | − const exportLine = auth | |
| 19 | − ? `${apiKeyEnvName}="<paste-api-key-from-above>"` | |
| 15 | + ? ` --header "x-api-key: ${auth.apiKey}"` | |
| 20 | 16 | : ""; |
| 21 | 17 | |
| 22 | 18 | return { |
| 23 | 19 | type: "command", |
| 24 | 20 | description: "Run once to install for all supported clients (via add-mcp):", |
| 25 | 21 | content: auth |
| 26 | − ? `${exportLine}\nnpx add-mcp "${mcpUrl}" --transport http --name "${serverName}" --all${headerArg}` | |
| 22 | + ? `npx add-mcp "${mcpUrl}" --transport http --name "${serverName}" --all${headerArg}` | |
| 27 | 23 | : `npx add-mcp "${mcpUrl}" --transport http --name "${serverName}" --all${headerArg}`, |
| 28 | 24 | }; |
| 29 | 25 | } |
| @@ -3,6 +3,7 @@ · 2 unmodified lines | ||
| 3 | 3 | import { useMemo, useState } from "react"; |
| 4 | 4 | import { useQuery } from "convex/react"; |
| 5 | 5 | import { Check, Copy } from "lucide-react"; |
| 6 | +import type { Id } from "@executor/database/convex/_generated/dataModel"; | |
| 6 | 7 | import { Button } from "@/components/ui/button"; |
| 7 | 8 | import { Input } from "@/components/ui/input"; |
| 8 | 9 | import { Label } from "@/components/ui/label"; |
| @@ -50,7 +51,7 @@ export function McpSetupCard({ · 41 unmodified lines | ||
| 50 | 51 | workspaceId, |
| 51 | 52 | sessionId, |
| 52 | 53 | }: { |
| 53 | − workspaceId?: string; | |
| 54 | + workspaceId?: Id<"workspaces">; | |
| 54 | 55 | sessionId?: string; |
| 55 | 56 | }) { |
| 56 | 57 | const [copiedKey, setCopiedKey] = useState<string | null>(null); |
| @@ -81,12 +82,12 @@ export function McpSetupCard({ · 24 unmodified lines | ||
| 81 | 82 | isAnonymousSession && workspaceId |
| 82 | 83 | ? { |
| 83 | 84 | workspaceId, |
| 84 | − sessionId, | |
| 85 | + sessionId | |
| 85 | 86 | } |
| 86 | 87 | : "skip", |
| 87 | 88 | ); |
| 88 | 89 | |
| 89 | − const apiKeyEnvName = anonymousMcpApiKey?.envVar ?? "API_KEY"; | |
| 90 | + | |
| 90 | 91 | const apiKeyValue = anonymousMcpApiKey?.enabled |
| 91 | 92 | ? anonymousMcpApiKey.apiKey |
| 92 | 93 | : null; |
| @@ -104,7 +105,6 @@ export function McpSetupCard({ · 11 unmodified lines | ||
| 104 | 105 | serverName, |
| 105 | 106 | isAnonymousSession |
| 106 | 107 | ? { |
| 107 | − apiKeyEnvName, | |
| 108 | 108 | apiKey: apiKeyValue ?? "your-api-key-here", |
| 109 | 109 | } |
| 110 | 110 | : undefined, |
| @@ -139,7 +139,6 @@ export function McpSetupCard({ · 28 unmodified lines | ||
| 139 | 139 | |
| 140 | 140 | {isAnonymousSession && ( |
| 141 | 141 | <div className="mt-2 space-y-1.5"> |
| 142 | − <Label className="text-xs text-muted-foreground">API Key ({apiKeyEnvName})</Label> | |
| 143 | 142 | <div className="flex items-center gap-2"> |
| 144 | 143 | <Input |
| 145 | 144 | value={apiKeyValue ?? "Loading API key..."} |
| @@ -130,8 +130,7 @@ test("getAuthKitUserProfile retries with normalized user id", async () => { · 129 unmodified lines | ||
| 130 | 130 | "workos|user_01KH1TVHS4WJCPQG2XQJGMMJMJ", |
| 131 | 131 | "user_01KH1TVHS4WJCPQG2XQJGMMJMJ", |
| 132 | 132 | ]); |
| 133 | − expect(result).toEqual({ | |
| 134 | − id: "user_01KH1TVHS4WJCPQG2XQJGMMJMJ", | |
| 135 | − email: "alex@example.com", | |
| 136 | − }); | |
| 133 | + expect(result).not.toBeNull(); | |
| 134 | + expect(result?.id).toBe("user_01KH1TVHS4WJCPQG2XQJGMMJMJ"); | |
| 135 | + expect(result?.email).toBe("alex@example.com"); | |
| 137 | 136 | }); |
| @@ -20,10 +20,9 @@ test("issueMcpApiKey + verifyMcpApiKey round trip", async () => { · 19 unmodified lines | ||
| 20 | 20 | |
| 21 | 21 | expect(apiKey).toBeTruthy(); |
| 22 | 22 | const verified = await verifyMcpApiKey(apiKey); |
| 23 | − expect(verified).toEqual({ | |
| 24 | − workspaceId: "workspace_123", | |
| 25 | − accountId: "account_456", | |
| 26 | − }); | |
| 23 | + expect(verified).not.toBeNull(); | |
| 24 | + expect(verified?.workspaceId).toBe("workspace_123" as Id<"workspaces">); | |
| 25 | + expect(verified?.accountId).toBe("account_456" as Id<"accounts">); | |
| 27 | 26 | }); |
| 28 | 27 | |
| 29 | 28 | test("verifyMcpApiKey rejects tampered token", async () => { |
| @@ -0,0 +1,25 @@ | ||
| 1 | +import { expect, test } from "bun:test"; | |
| 2 | +import { derivePersonalNames } from "./naming"; | |
| 3 | + | |
| 4 | +test("derivePersonalNames ignores generated fallback full name", () => { | |
| 5 | + const names = derivePersonalNames({ | |
| 6 | + fullName: "User GMMJMJ", | |
| 7 | + email: "alex@example.com", | |
| 8 | + workosUserId: "user_01KH1TVHS4WJCPQG2XQJGMMJMJ", | |
| 9 | + }); | |
| 10 | + | |
| 11 | + expect(names.organizationName).toBe("Alex's Organization"); | |
| 12 | + expect(names.workspaceName).toBe("Alex's Workspace"); | |
| 13 | +}); | |
| 14 | + | |
| 15 | +test("derivePersonalNames prefers first name when available", () => { | |
| 16 | + const names = derivePersonalNames({ | |
| 17 | + firstName: "Alex", | |
| 18 | + fullName: "User GMMJMJ", | |
| 19 | + email: "alex@example.com", | |
| 20 | + workosUserId: "user_01KH1TVHS4WJCPQG2XQJGMMJMJ", | |
| 21 | + }); | |
| 22 | + | |
| 23 | + expect(names.organizationName).toBe("Alex's Organization"); | |
| 24 | + expect(names.workspaceName).toBe("Alex's Workspace"); | |
| 25 | +}); | |
| @@ -14,6 +14,29 @@ function escapeRegex(value: string): string { · 13 unmodified lines | ||
| 14 | 14 | return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | +function isGeneratedIdentityLabel(value: string, workosUserId: string): boolean { | |
| 18 | + const normalizedValue = value.trim(); | |
| 19 | + const normalizedWorkosUserId = workosUserId.trim(); | |
| 20 | + if (!normalizedValue) { | |
| 21 | + return false; | |
| 22 | + } | |
| 23 | + | |
| 24 | + const fallbackSuffix = normalizedWorkosUserId.slice(-6); | |
| 25 | + if (fallbackSuffix && new RegExp(`^user\\s+${escapeRegex(fallbackSuffix)}$`, "i").test(normalizedValue)) { | |
| 26 | + return true; | |
| 27 | + } | |
| 28 | + | |
| 29 | + if (/^user\s+[a-z0-9]{6,}$/i.test(normalizedValue)) { | |
| 30 | + return true; | |
| 31 | + } | |
| 32 | + | |
| 33 | + if (/^(?:workos\|)?user_[a-z0-9]+$/i.test(normalizedValue)) { | |
| 34 | + return true; | |
| 35 | + } | |
| 36 | + | |
| 37 | + return normalizedValue.toLowerCase() === normalizedWorkosUserId.toLowerCase(); | |
| 38 | +} | |
| 39 | + | |
| 17 | 40 | export function isGeneratedPersonalOrganizationName(name: string, workosUserId: string): boolean { |
| 18 | 41 | if (/workspace$/i.test(name)) { |
| 19 | 42 | return true; |
| @@ -40,12 +63,12 @@ export function isGeneratedPersonalWorkspaceName(name: string, workosUserId: str · 20 unmodified lines | ||
| 40 | 63 | |
| 41 | 64 | function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email?: string; workosUserId: string }): string { |
| 42 | 65 | const firstName = args.firstName?.trim(); |
| 43 | − if (firstName && !/^my$/i.test(firstName)) { | |
| 66 | + if (firstName && !/^my$/i.test(firstName) && !/^user$/i.test(firstName)) { | |
| 44 | 67 | return firstName; |
| 45 | 68 | } |
| 46 | 69 | |
| 47 | 70 | const fullName = args.fullName?.trim(); |
| 48 | − if (fullName && !fullName.includes("@")) { | |
| 71 | + if (fullName && !fullName.includes("@") && !isGeneratedIdentityLabel(fullName, args.workosUserId)) { | |
| 49 | 72 | return fullName; |
| 50 | 73 | } |
| 51 | 74 | |