| @@ -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 = { | 18 | import { env } from "cloudflare:workers"; |
| 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 | }; | | |
| 27 | | 19 | |
| 28 | type SharedEnv = Readonly<{ | 20 | type SharedEnv = Readonly<{ |
| 29 | NODE_ENV: "development" | "test" | "production"; | 21 | NODE_ENV: "development" | "test" | "production"; |
| @@ -48,22 +40,50 @@ type ServerEnv = SharedEnv & · 18 unmodified lines |
| 48 | | 40 | |
| 49 | type WebEnv = Readonly<Record<string, never>>; | 41 | type WebEnv = Readonly<Record<string, never>>; |
| 50 | | 42 | |
| 51 | export const shared = createEnv(sharedShape, { | 43 | const SERVER_DEFAULTS: Record<keyof ServerEnv, string> = { |
| 52 | runtimeEnv: process.env, | 44 | NODE_ENV: "development", |
| 53 | emptyStringAsUndefined: true, | 45 | DATABASE_URL: "", |
| 54 | }) as SharedEnv; | 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( | 79 | const makeEnvProxy = <T extends object>(defaults: Record<string, string>): T => |
| 57 | {}, | 80 | new Proxy({} as T, { |
| 58 | { | 81 | get: (_target, key) => { |
| 59 | prefix: "PUBLIC_", | 82 | if (typeof key !== "string") return undefined; |
| 60 | runtimeEnv: process.env, | 83 | return read(key) ?? defaults[key] ?? ""; |
| 61 | emptyStringAsUndefined: true, | 84 | }, |
| 62 | }, | 85 | }); |
| 63 | ) as WebEnv; | | |
| 64 | | 86 | |
| 65 | export const server = createEnv(serverShape, { | 87 | export const shared: SharedEnv = makeEnvProxy<SharedEnv>(SHARED_DEFAULTS); |
| 66 | extends: [shared], | 88 | export const server: ServerEnv = makeEnvProxy<ServerEnv>(SERVER_DEFAULTS); |
| 67 | runtimeEnv: process.env, | 89 | export const web: WebEnv = {} as WebEnv; |
| 68 | emptyStringAsUndefined: true, | | |
| 69 | }) as ServerEnv; | | |