Unified · Split
| @@ -1,6 +1,7 @@ | |||
| 1 | import fs from "node:fs/promises"; | 1 | import fs from "node:fs/promises"; |
| 2 | import os from "node:os"; | 2 | import os from "node:os"; |
| 3 | import path from "node:path"; | 3 | import path from "node:path"; |
| 4 | import { generateKeyPairSync } from "node:crypto"; | ||
| 4 | import { z } from "zod"; | 5 | import { z } from "zod"; |
| 5 | 6 | ||
| 6 | import type { ManagedRuntimeInfo } from "../managed-runtime"; | 7 | import type { ManagedRuntimeInfo } from "../managed-runtime"; |
| @@ -37,6 +38,151 @@ const BOOTSTRAP_REQUIRED_DEPENDENCIES: BootstrapDependency[] = [ · 30 unmodified lines | |||
| 37 | { packageName: "@modelcontextprotocol/sdk" }, | 38 | { packageName: "@modelcontextprotocol/sdk" }, |
| 38 | ]; | 39 | ]; |
| 39 | 40 | ||
| 41 | const managedAnonymousAuthEnvFileName = "managed-anonymous-auth.json"; | ||
| 42 | |||
| 43 | type ManagedAnonymousAuthEnv = { | ||
| 44 | ANONYMOUS_AUTH_PRIVATE_KEY_PEM: string; | ||
| 45 | ANONYMOUS_AUTH_PUBLIC_KEY_PEM: string; | ||
| 46 | MCP_API_KEY_SECRET: string; | ||
| 47 | }; | ||
| 48 | |||
| 49 | function trimEnv(name: string): string | null { | ||
| 50 | const value = process.env[name]?.trim(); | ||
| 51 | return value && value.length > 0 ? value : null; | ||
| 52 | } | ||
| 53 | |||
| 54 | function normalizePemForEnv(value: string): string { | ||
| 55 | return value.replace(/\r\n/g, "\n").replace(/\n/g, "\\n").trim(); | ||
| 56 | } | ||
| 57 | |||
| 58 | function resolveAnonymousAuthFromProcessEnv(): ManagedAnonymousAuthEnv | null { | ||
| 59 | const privateKeyPem = trimEnv("ANONYMOUS_AUTH_PRIVATE_KEY_PEM"); | ||
| 60 | const publicKeyPem = trimEnv("ANONYMOUS_AUTH_PUBLIC_KEY_PEM"); | ||
| 61 | if (!privateKeyPem || !publicKeyPem) { | ||
| 62 | return null; | ||
| 63 | } | ||
| 64 | |||
| 65 | const apiKeySecret = trimEnv("MCP_API_KEY_SECRET") ?? privateKeyPem; | ||
| 66 | return { | ||
| 67 | ANONYMOUS_AUTH_PRIVATE_KEY_PEM: normalizePemForEnv(privateKeyPem), | ||
| 68 | ANONYMOUS_AUTH_PUBLIC_KEY_PEM: normalizePemForEnv(publicKeyPem), | ||
| 69 | MCP_API_KEY_SECRET: normalizePemForEnv(apiKeySecret), | ||
| 70 | }; | ||
| 71 | } | ||
| 72 | |||
| 73 | async function readManagedAnonymousAuthEnv(info: ManagedRuntimeInfo): Promise<ManagedAnonymousAuthEnv | null> { | ||
| 74 | const filePath = path.join(info.rootDir, managedAnonymousAuthEnvFileName); | ||
| 75 | try { | ||
| 76 | const raw = await fs.readFile(filePath, "utf8"); | ||
| 77 | const parsed = JSON.parse(raw) as Partial<ManagedAnonymousAuthEnv>; | ||
| 78 | |||
| 79 | if ( | ||
| 80 | typeof parsed.ANONYMOUS_AUTH_PRIVATE_KEY_PEM !== "string" | ||
| 81 | || typeof parsed.ANONYMOUS_AUTH_PUBLIC_KEY_PEM !== "string" | ||
| 82 | || typeof parsed.MCP_API_KEY_SECRET !== "string" | ||
| 83 | ) { | ||
| 84 | return null; | ||
| 85 | } | ||
| 86 | |||
| 87 | if ( | ||
| 88 | parsed.ANONYMOUS_AUTH_PRIVATE_KEY_PEM.trim().length === 0 | ||
| 89 | || parsed.ANONYMOUS_AUTH_PUBLIC_KEY_PEM.trim().length === 0 | ||
| 90 | || parsed.MCP_API_KEY_SECRET.trim().length === 0 | ||
| 91 | ) { | ||
| 92 | return null; | ||
| 93 | } | ||
| 94 | |||
| 95 | return { | ||
| 96 | ANONYMOUS_AUTH_PRIVATE_KEY_PEM: parsed.ANONYMOUS_AUTH_PRIVATE_KEY_PEM.trim(), | ||
| 97 | ANONYMOUS_AUTH_PUBLIC_KEY_PEM: parsed.ANONYMOUS_AUTH_PUBLIC_KEY_PEM.trim(), | ||
| 98 | MCP_API_KEY_SECRET: parsed.MCP_API_KEY_SECRET.trim(), | ||
| 99 | }; | ||
| 100 | } catch { | ||
| 101 | return null; | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | function generateManagedAnonymousAuthEnv(): ManagedAnonymousAuthEnv { | ||
| 106 | const keyPair = generateKeyPairSync("ec", { | ||
| 107 | namedCurve: "P-256", | ||
| 108 | privateKeyEncoding: { | ||
| 109 | type: "pkcs8", | ||
| 110 | format: "pem", | ||
| 111 | }, | ||
| 112 | publicKeyEncoding: { | ||
| 113 | type: "spki", | ||
| 114 | format: "pem", | ||
| 115 | }, | ||
| 116 | }); | ||
| 117 | |||
| 118 | const privateKeyPem = normalizePemForEnv(keyPair.privateKey); | ||
| 119 | return { | ||
| 120 | ANONYMOUS_AUTH_PRIVATE_KEY_PEM: privateKeyPem, | ||
| 121 | ANONYMOUS_AUTH_PUBLIC_KEY_PEM: normalizePemForEnv(keyPair.publicKey), | ||
| 122 | MCP_API_KEY_SECRET: normalizePemForEnv(trimEnv("MCP_API_KEY_SECRET") ?? privateKeyPem), | ||
| 123 | }; | ||
| 124 | } | ||
| 125 | |||
| 126 | async function writeManagedAnonymousAuthEnv(info: ManagedRuntimeInfo, env: ManagedAnonymousAuthEnv): Promise<void> { | ||
| 127 | const filePath = path.join(info.rootDir, managedAnonymousAuthEnvFileName); | ||
| 128 | await fs.mkdir(info.rootDir, { recursive: true }); | ||
| 129 | await fs.writeFile(filePath, `${JSON.stringify(env, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); | ||
| 130 | } | ||
| 131 | |||
| 132 | async function resolveManagedAnonymousAuthEnv(info: ManagedRuntimeInfo): Promise<ManagedAnonymousAuthEnv> { | ||
| 133 | const fromProcess = resolveAnonymousAuthFromProcessEnv(); | ||
| 134 | if (fromProcess) { | ||
| 135 | return fromProcess; | ||
| 136 | } | ||
| 137 | |||
| 138 | const fromDisk = await readManagedAnonymousAuthEnv(info); | ||
| 139 | if (fromDisk) { | ||
| 140 | if (trimEnv("MCP_API_KEY_SECRET")) { | ||
| 141 | return { | ||
| 142 | ...fromDisk, | ||
| 143 | MCP_API_KEY_SECRET: normalizePemForEnv(trimEnv("MCP_API_KEY_SECRET") ?? fromDisk.MCP_API_KEY_SECRET), | ||
| 144 | }; | ||
| 145 | } | ||
| 146 | return fromDisk; | ||
| 147 | } | ||
| 148 | |||
| 149 | const generated = generateManagedAnonymousAuthEnv(); | ||
| 150 | await writeManagedAnonymousAuthEnv(info, generated); | ||
| 151 | return generated; | ||
| 152 | } | ||
| 153 | |||
| 154 | async function seedManagedRuntimeEnvVars( | ||
| 155 | info: ManagedRuntimeInfo, | ||
| 156 | projectDir: string, | ||
| 157 | envFilePath: string, | ||
| 158 | ): Promise<void> { | ||
| 159 | const anonymousAuthEnv = await resolveManagedAnonymousAuthEnv(info); | ||
| 160 | const envEntries: Array<{ name: string; value: string }> = [ | ||
| 161 | { name: "WORKOS_CLIENT_ID", value: "disabled" }, | ||
| 162 | { name: "ANONYMOUS_AUTH_PRIVATE_KEY_PEM", value: anonymousAuthEnv.ANONYMOUS_AUTH_PRIVATE_KEY_PEM }, | ||
| 163 | { name: "ANONYMOUS_AUTH_PUBLIC_KEY_PEM", value: anonymousAuthEnv.ANONYMOUS_AUTH_PUBLIC_KEY_PEM }, | ||
| 164 | { name: "MCP_API_KEY_SECRET", value: anonymousAuthEnv.MCP_API_KEY_SECRET }, | ||
| 165 | ]; | ||
| 166 | |||
| 167 | for (const entry of envEntries) { | ||
| 168 | const seeded = await runManagedConvexCli( | ||
| 169 | info, | ||
| 170 | projectDir, | ||
| 171 | ["env", "set", entry.name, entry.value], | ||
| 172 | envFilePath, | ||
| 173 | { | ||
| 174 | stdout: "pipe", | ||
| 175 | stderr: "pipe", | ||
| 176 | }, | ||
| 177 | ); | ||
| 178 | |||
| 179 | if (seeded.exitCode !== 0) { | ||
| 180 | const detail = seeded.stderr.trim() || seeded.stdout.trim() || "unknown error"; | ||
| 181 | console.warn(`[executor] could not seed ${entry.name} in local backend env: ${detail}`); | ||
| 182 | } | ||
| 183 | } | ||
| 184 | } | ||
| 185 | |||
| 40 | async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<string> { | 186 | async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<string> { |
| 41 | const response = await fetch("https://api.convex.dev/api/local_deployment/generate_admin_key", { | 187 | const response = await fetch("https://api.convex.dev/api/local_deployment/generate_admin_key", { |
| 42 | method: "POST", | 188 | method: "POST", |
| @@ -281,20 +427,7 @@ export async function checkBootstrapHealth(info: ManagedRuntimeInfo): Promise<Bo · 238 unmodified lines | |||
| 281 | const envFilePath = await writeBootstrapEnvFile(info, adminKey); | 427 | const envFilePath = await writeBootstrapEnvFile(info, adminKey); |
| 282 | 428 | ||
| 283 | try { | 429 | try { |
| 284 | const ensureWorkosClientId = await runManagedConvexCli( | 430 | await seedManagedRuntimeEnvVars(info, projectDir, envFilePath); |
| 285 | info, | ||
| 286 | projectDir, | ||
| 287 | ["env", "set", "WORKOS_CLIENT_ID", "disabled"], | ||
| 288 | envFilePath, | ||
| 289 | { | ||
| 290 | stdout: "pipe", | ||
| 291 | stderr: "pipe", | ||
| 292 | }, | ||
| 293 | ); | ||
| 294 | if (ensureWorkosClientId.exitCode !== 0) { | ||
| 295 | const detail = ensureWorkosClientId.stderr.trim() || ensureWorkosClientId.stdout.trim() || "unknown error"; | ||
| 296 | console.warn(`[executor] could not seed WORKOS_CLIENT_ID in local backend env: ${detail}`); | ||
| 297 | } | ||
| 298 | 431 | ||
| 299 | const check = await runManagedConvexCli(info, projectDir, ["run", "app:getClientConfig"], envFilePath, { | 432 | const check = await runManagedConvexCli(info, projectDir, ["run", "app:getClientConfig"], envFilePath, { |
| 300 | stdout: "pipe", | 433 | stdout: "pipe", |
| @@ -348,6 +481,8 @@ export async function ensureProjectBootstrapped(info: ManagedRuntimeInfo): Promi · 47 unmodified lines | |||
| 348 | const envFilePath = await writeBootstrapEnvFile(info, adminKey); | 481 | const envFilePath = await writeBootstrapEnvFile(info, adminKey); |
| 349 | 482 | ||
| 350 | try { | 483 | try { |
| 484 | await seedManagedRuntimeEnvVars(info, projectDir, envFilePath); | ||
| 485 | |||
| 351 | console.log("[executor] bootstrapping Convex functions to local backend"); | 486 | console.log("[executor] bootstrapping Convex functions to local backend"); |
| 352 | const deploy = await runManagedConvexCli( | 487 | const deploy = await runManagedConvexCli( |
| 353 | info, | 488 | info, |