| @@ -1,29 +1,21 @@ |
| 1 | | −import { createEnv, Env } from "@executor/env"; |
| 1 | +// --------------------------------------------------------------------------- |
| 2 | +// Env accessors for apps/cloud. |
| 3 | +// --------------------------------------------------------------------------- |
| 4 | +// |
| 5 | +// `server` / `shared` are lazy Proxies over `cloudflare:workers` `env`, with |
| 6 | +// a `process.env` fallback for the node-pool tests (which stub |
| 7 | +// `cloudflare:workers` to an empty `env` and rely on vitest to populate |
| 8 | +// `process.env`). Reads happen at the call site, not at module load, so |
| 9 | +// secrets land correctly in both the edge isolate and the Durable Object |
| 10 | +// isolate — the latter previously captured empty strings at env.ts |
| 11 | +// module-load time and silently broke `DoTelemetryLive`, `AutumnService` |
| 12 | +// billing writes, and likely other DO-reachable secret readers. |
| 13 | +// |
| 14 | +// Types below mirror the prior `createEnv(...)`-derived shapes so every |
| 15 | +// caller (`server.X`) keeps the same typed surface. |
| 16 | +// --------------------------------------------------------------------------- |
| 2 | 17 | |
| 3 | | −const sharedShape = { |
| 4 | | − NODE_ENV: Env.literalOr("NODE_ENV", "development", "development", "test", "production"), |
| 5 | | −}; |
| 6 | | − |
| 7 | | −const serverShape = { |
| 8 | | − DATABASE_URL: Env.stringOr("DATABASE_URL", ""), |
| 9 | | − MCP_SESSION_REQUEST_SCOPED_RUNTIME: Env.literalOr( |
| 10 | | − "MCP_SESSION_REQUEST_SCOPED_RUNTIME", |
| 11 | | − "false", |
| 12 | | − "false", |
| 13 | | − "true", |
| 14 | | − ), |
| 15 | | − WORKOS_API_KEY: Env.string("WORKOS_API_KEY"), |
| 16 | | − WORKOS_CLIENT_ID: Env.string("WORKOS_CLIENT_ID"), |
| 17 | | − WORKOS_COOKIE_PASSWORD: Env.string("WORKOS_COOKIE_PASSWORD"), |
| 18 | | − VITE_PUBLIC_SITE_URL: Env.stringOr("VITE_PUBLIC_SITE_URL", ""), |
| 19 | | − MCP_AUTHKIT_DOMAIN: Env.stringOr("MCP_AUTHKIT_DOMAIN", "https://signin.executor.sh"), |
| 20 | | − MCP_RESOURCE_ORIGIN: Env.stringOr("MCP_RESOURCE_ORIGIN", "https://executor.sh"), |
| 21 | | − AUTUMN_SECRET_KEY: Env.stringOr("AUTUMN_SECRET_KEY", ""), |
| 22 | | − SENTRY_DSN: Env.stringOr("SENTRY_DSN", ""), |
| 23 | | − AXIOM_TOKEN: Env.stringOr("AXIOM_TOKEN", ""), |
| 24 | | − AXIOM_DATASET: Env.stringOr("AXIOM_DATASET", "executor-cloud"), |
| 25 | | − AXIOM_TRACES_URL: Env.stringOr("AXIOM_TRACES_URL", "https://api.axiom.co/v1/traces"), |
| 26 | | −}; |
| 18 | +import { env } from "cloudflare:workers"; |
| 27 | 19 | |
| 28 | 20 | type SharedEnv = Readonly<{ |
| 29 | 21 | NODE_ENV: "development" | "test" | "production"; |
| @@ -48,22 +40,50 @@ type ServerEnv = SharedEnv & · 18 unmodified lines |
| 48 | 40 | |
| 49 | 41 | type WebEnv = Readonly<Record<string, never>>; |
| 50 | 42 | |
| 51 | | −export const shared = createEnv(sharedShape, { |
| 52 | | − runtimeEnv: process.env, |
| 53 | | − emptyStringAsUndefined: true, |
| 54 | | −}) as SharedEnv; |
| 43 | +const SERVER_DEFAULTS: Record<keyof ServerEnv, string> = { |
| 44 | + NODE_ENV: "development", |
| 45 | + DATABASE_URL: "", |
| 46 | + MCP_SESSION_REQUEST_SCOPED_RUNTIME: "false", |
| 47 | + WORKOS_API_KEY: "", |
| 48 | + WORKOS_CLIENT_ID: "", |
| 49 | + WORKOS_COOKIE_PASSWORD: "", |
| 50 | + VITE_PUBLIC_SITE_URL: "", |
| 51 | + MCP_AUTHKIT_DOMAIN: "https://signin.executor.sh", |
| 52 | + MCP_RESOURCE_ORIGIN: "https://executor.sh", |
| 53 | + AUTUMN_SECRET_KEY: "", |
| 54 | + SENTRY_DSN: "", |
| 55 | + AXIOM_TOKEN: "", |
| 56 | + AXIOM_DATASET: "executor-cloud", |
| 57 | + AXIOM_TRACES_URL: "https://api.axiom.co/v1/traces", |
| 58 | +}; |
| 59 | + |
| 60 | +const SHARED_DEFAULTS: Record<keyof SharedEnv, string> = { |
| 61 | + NODE_ENV: "development", |
| 62 | +}; |
| 63 | + |
| 64 | +const readFromWorkerEnv = (key: string): string | undefined => { |
| 65 | + const bag = env as unknown as Record<string, unknown>; |
| 66 | + const v = bag[key]; |
| 67 | + return typeof v === "string" && v.length > 0 ? v : undefined; |
| 68 | +}; |
| 69 | + |
| 70 | +const readFromProcessEnv = (key: string): string | undefined => { |
| 71 | + if (typeof process === "undefined" || !process.env) return undefined; |
| 72 | + const v = process.env[key]; |
| 73 | + return typeof v === "string" && v.length > 0 ? v : undefined; |
| 74 | +}; |
| 75 | + |
| 76 | +const read = (key: string): string | undefined => |
| 77 | + readFromWorkerEnv(key) ?? readFromProcessEnv(key); |
| 55 | 78 | |
| 56 | | −export const web = createEnv( |
| 57 | | − {}, |
| 58 | | − { |
| 59 | | − prefix: "PUBLIC_", |
| 60 | | − runtimeEnv: process.env, |
| 61 | | − emptyStringAsUndefined: true, |
| 62 | | − }, |
| 63 | | −) as WebEnv; |
| 79 | +const makeEnvProxy = <T extends object>(defaults: Record<string, string>): T => |
| 80 | + new Proxy({} as T, { |
| 81 | + get: (_target, key) => { |
| 82 | + if (typeof key !== "string") return undefined; |
| 83 | + return read(key) ?? defaults[key] ?? ""; |
| 84 | + }, |
| 85 | + }); |
| 64 | 86 | |
| 65 | | −export const server = createEnv(serverShape, { |
| 66 | | − extends: [shared], |
| 67 | | − runtimeEnv: process.env, |
| 68 | | − emptyStringAsUndefined: true, |
| 69 | | −}) as ServerEnv; |
| 87 | +export const shared: SharedEnv = makeEnvProxy<SharedEnv>(SHARED_DEFAULTS); |
| 88 | +export const server: ServerEnv = makeEnvProxy<ServerEnv>(SERVER_DEFAULTS); |
| 89 | +export const web: WebEnv = {} as WebEnv; |