Unified · Split
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import { Sandbox } from "@vercel/sandbox"; |
| 2 | +import { generateKeyPairSync } from "node:crypto"; | |
| 2 | 3 | |
| 3 | 4 | type CommandResult = { |
| 4 | 5 | exitCode: number; |
| @@ -32,6 +33,52 @@ function sandboxCredentials(): { token: string; teamId: string; projectId: strin · 27 unmodified lines | ||
| 32 | 33 | return {}; |
| 33 | 34 | } |
| 34 | 35 | |
| 36 | +function normalizePemForEnv(value: string): string { | |
| 37 | + return value.replace(/\r\n/g, "\n").replace(/\n/g, "\\n").trim(); | |
| 38 | +} | |
| 39 | + | |
| 40 | +function resolveAnonymousAuthEnv(): { | |
| 41 | + privateKeyPem: string; | |
| 42 | + publicKeyPem: string; | |
| 43 | + apiKeySecret: string; | |
| 44 | + generated: boolean; | |
| 45 | +} { | |
| 46 | + const privateFromEnv = process.env.ANONYMOUS_AUTH_PRIVATE_KEY_PEM?.trim(); | |
| 47 | + const publicFromEnv = process.env.ANONYMOUS_AUTH_PUBLIC_KEY_PEM?.trim(); | |
| 48 | + const apiKeyFromEnv = process.env.MCP_API_KEY_SECRET?.trim(); | |
| 49 | + | |
| 50 | + if (privateFromEnv && publicFromEnv) { | |
| 51 | + return { | |
| 52 | + privateKeyPem: normalizePemForEnv(privateFromEnv), | |
| 53 | + publicKeyPem: normalizePemForEnv(publicFromEnv), | |
| 54 | + apiKeySecret: apiKeyFromEnv && apiKeyFromEnv.length > 0 ? apiKeyFromEnv : normalizePemForEnv(privateFromEnv), | |
| 55 | + generated: false, | |
| 56 | + }; | |
| 57 | + } | |
| 58 | + | |
| 59 | + const keyPair = generateKeyPairSync("ec", { | |
| 60 | + namedCurve: "P-256", | |
| 61 | + privateKeyEncoding: { | |
| 62 | + type: "pkcs8", | |
| 63 | + format: "pem", | |
| 64 | + }, | |
| 65 | + publicKeyEncoding: { | |
| 66 | + type: "spki", | |
| 67 | + format: "pem", | |
| 68 | + }, | |
| 69 | + }); | |
| 70 | + | |
| 71 | + const privateKeyPem = normalizePemForEnv(keyPair.privateKey); | |
| 72 | + const publicKeyPem = normalizePemForEnv(keyPair.publicKey); | |
| 73 | + | |
| 74 | + return { | |
| 75 | + privateKeyPem, | |
| 76 | + publicKeyPem, | |
| 77 | + apiKeySecret: apiKeyFromEnv && apiKeyFromEnv.length > 0 ? apiKeyFromEnv : privateKeyPem, | |
| 78 | + generated: true, | |
| 79 | + }; | |
| 80 | +} | |
| 81 | + | |
| 35 | 82 | async function runSandboxBash( |
| 36 | 83 | sandbox: Sandbox, |
| 37 | 84 | script: string, |
| @@ -104,6 +151,11 @@ try { · 66 unmodified lines | ||
| 104 | 151 | console.log(`[sandbox] created: ${sandbox.sandboxId}`); |
| 105 | 152 | console.log("[sandbox] running executor.sh install flow..."); |
| 106 | 153 | |
| 154 | + const anonymousAuthEnv = resolveAnonymousAuthEnv(); | |
| 155 | + if (anonymousAuthEnv.generated) { | |
| 156 | + console.log("[sandbox] generated ephemeral anonymous auth keys for this session"); | |
| 157 | + } | |
| 158 | + | |
| 107 | 159 | const install = await runSandboxBash( |
| 108 | 160 | sandbox, |
| 109 | 161 | [ |
| @@ -122,6 +174,9 @@ try { · 12 unmodified lines | ||
| 122 | 174 | EXECUTOR_BACKEND_PORT: String(backendPort), |
| 123 | 175 | EXECUTOR_BACKEND_SITE_PORT: String(sitePort), |
| 124 | 176 | EXECUTOR_WEB_PORT: String(webPort), |
| 177 | + ANONYMOUS_AUTH_PRIVATE_KEY_PEM: anonymousAuthEnv.privateKeyPem, | |
| 178 | + ANONYMOUS_AUTH_PUBLIC_KEY_PEM: anonymousAuthEnv.publicKeyPem, | |
| 179 | + MCP_API_KEY_SECRET: anonymousAuthEnv.apiKeySecret, | |
| 125 | 180 | }, |
| 126 | 181 | }, |
| 127 | 182 | ); |
| @@ -139,6 +194,7 @@ try { · 11 unmodified lines | ||
| 139 | 194 | console.log(`Convex Site: ${convexSiteUrl}`); |
| 140 | 195 | console.log(`MCP (auth): ${convexSiteUrl}/mcp`); |
| 141 | 196 | console.log(`MCP (anonymous): ${convexSiteUrl}/mcp/anonymous`); |
| 197 | + console.log("Tip: open Web UI and create an anonymous organization to get workspace context + API key."); | |
| 142 | 198 | console.log(""); |
| 143 | 199 | console.log("The sandbox is left running so you can test from your machine."); |
| 144 | 200 | console.log("Stop it from Vercel when you are done, or wait for sandbox timeout."); |