History

seed anonymous auth env during runtime bootstrap

Rhys Sullivan committed 8 months ago

Commit e34ac2e1 parent 6c56df71 file · +149 −14Browse files

Unified · Split

executor/packages/core/src/managed/runtime-bootstrap.ts+149−14
@@ -1,6 +1,7 @@
11 import fs from "node:fs/promises";
22 import os from "node:os";
33 import path from "node:path";
4+import { generateKeyPairSync } from "node:crypto";
45 import { z } from "zod";
56
67 import type { ManagedRuntimeInfo } from "../managed-runtime";
@@ -37,6 +38,151 @@ const BOOTSTRAP_REQUIRED_DEPENDENCIES: BootstrapDependency[] = [ · 30 unmodified lines
3738 { packageName: "@modelcontextprotocol/sdk" },
3839 ];
3940
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+
40186 async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<string> {
41187 const response = await fetch("https://api.convex.dev/api/local_deployment/generate_admin_key", {
42188 method: "POST",
@@ -281,20 +427,7 @@ export async function checkBootstrapHealth(info: ManagedRuntimeInfo): Promise<Bo · 238 unmodified lines
281427 const envFilePath = await writeBootstrapEnvFile(info, adminKey);
282428
283429 try {
284− const ensureWorkosClientId = await runManagedConvexCli(
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− }
430+ await seedManagedRuntimeEnvVars(info, projectDir, envFilePath);
298431
299432 const check = await runManagedConvexCli(info, projectDir, ["run", "app:getClientConfig"], envFilePath, {
300433 stdout: "pipe",
@@ -348,6 +481,8 @@ export async function ensureProjectBootstrapped(info: ManagedRuntimeInfo): Promi · 47 unmodified lines
348481 const envFilePath = await writeBootstrapEnvFile(info, adminKey);
349482
350483 try {
484+ await seedManagedRuntimeEnvVars(info, projectDir, envFilePath);
485+
351486 console.log("[executor] bootstrapping Convex functions to local backend");
352487 const deploy = await runManagedConvexCli(
353488 info,