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