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 @@
1import fs from "node:fs/promises";1import fs from "node:fs/promises";
2import os from "node:os";2import os from "node:os";
3import path from "node:path";3import path from "node:path";
4import { generateKeyPairSync } from "node:crypto";
4import { z } from "zod";5import { z } from "zod";
5 6
6import type { ManagedRuntimeInfo } from "../managed-runtime";7import 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
41const managedAnonymousAuthEnvFileName = "managed-anonymous-auth.json";
42
43type ManagedAnonymousAuthEnv = {
44 ANONYMOUS_AUTH_PRIVATE_KEY_PEM: string;
45 ANONYMOUS_AUTH_PUBLIC_KEY_PEM: string;
46 MCP_API_KEY_SECRET: string;
47};
48
49function trimEnv(name: string): string | null {
50 const value = process.env[name]?.trim();
51 return value && value.length > 0 ? value : null;
52}
53
54function normalizePemForEnv(value: string): string {
55 return value.replace(/\r\n/g, "\n").replace(/\n/g, "\\n").trim();
56}
57
58function 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
73async 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
105function 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
126async 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
132async 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
154async 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
40async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<string> {186async 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,