diff --git a/apps/cloud/package.json b/apps/cloud/package.json index 12a52a6537..9546af8988 100644 --- a/apps/cloud/package.json +++ b/apps/cloud/package.json @@ -8,6 +8,7 @@ "@effect-atom/atom-react": "^0.5.0", "@effect/platform": "catalog:", "@executor/api": "workspace:*", + "@executor/env": "workspace:*", "@executor/execution": "workspace:*", "@executor/plugin-google-discovery": "workspace:*", "@executor/plugin-graphql": "workspace:*", diff --git a/apps/cloud/src/api.ts b/apps/cloud/src/api.ts index 1fc1182f74..da5dcaf7e9 100644 --- a/apps/cloud/src/api.ts +++ b/apps/cloud/src/api.ts @@ -31,6 +31,7 @@ import { CloudAuthHandlers, CloudAuthPublicHandlers } from "./auth/handlers"; import { WorkOSAuth } from "./auth/workos"; import { DbService } from "./services/db"; import { createTeamExecutor } from "./services/executor"; +import { server } from "./env"; const ProtectedCloudApi = addGroup(OpenApiGroup) .add(McpGroup) @@ -104,7 +105,7 @@ const COOKIE_OPTIONS = { httpOnly: true, sameSite: "lax" as const, maxAge: 60 * 60 * 24 * 7, - secure: process.env.NODE_ENV === "production", + secure: server.NODE_ENV === "production", }; const resolveAuth = (request: Request) => @@ -151,7 +152,7 @@ const resolveExecutor = (teamId: string) => const users = yield* UserStoreService; const team = yield* users.use((store) => store.getTeam(teamId)); const teamName = team?.name ?? "Unknown Team"; - const encryptionKey = process.env.ENCRYPTION_KEY ?? "local-dev-encryption-key"; + const encryptionKey = server.ENCRYPTION_KEY; return yield* createTeamExecutor(teamId, teamName, encryptionKey); }); diff --git a/apps/cloud/src/auth/handlers.ts b/apps/cloud/src/auth/handlers.ts index ca1e40c4cb..049281c1d0 100644 --- a/apps/cloud/src/auth/handlers.ts +++ b/apps/cloud/src/auth/handlers.ts @@ -6,13 +6,14 @@ import { addGroup } from "@executor/api"; import { AUTH_PATHS, CloudAuthApi, CloudAuthPublicApi } from "./api"; import { AuthContext, UserStoreService } from "./context"; import { WorkOSAuth } from "./workos"; +import { server } from "../env"; const COOKIE_OPTIONS = { path: "/", httpOnly: true, sameSite: "lax" as const, maxAge: 60 * 60 * 24 * 7, - secure: process.env.NODE_ENV === "production", + secure: server.NODE_ENV === "production", }; // --------------------------------------------------------------------------- diff --git a/apps/cloud/src/auth/workos.ts b/apps/cloud/src/auth/workos.ts index 0572edb6eb..ae7ddc3294 100644 --- a/apps/cloud/src/auth/workos.ts +++ b/apps/cloud/src/auth/workos.ts @@ -5,6 +5,7 @@ import { Context, Effect, Layer } from "effect"; import { WorkOS } from "@workos-inc/node"; import { WorkOSError } from "./errors"; +import { server } from "../env"; const COOKIE_NAME = "wos-session"; @@ -14,9 +15,9 @@ const COOKIE_NAME = "wos-session"; const make = Effect.gen(function* () { - const apiKey = process.env.WORKOS_API_KEY!; - const clientId = process.env.WORKOS_CLIENT_ID!; - const cookiePassword = process.env.WORKOS_COOKIE_PASSWORD!; + const apiKey = server.WORKOS_API_KEY; + const clientId = server.WORKOS_CLIENT_ID; + const cookiePassword = server.WORKOS_COOKIE_PASSWORD; if (!cookiePassword || cookiePassword.length < 32) { return yield* Effect.die(new Error("WORKOS_COOKIE_PASSWORD must be at least 32 characters")); diff --git a/apps/cloud/src/env.ts b/apps/cloud/src/env.ts new file mode 100644 index 0000000000..1466de9a92 --- /dev/null +++ b/apps/cloud/src/env.ts @@ -0,0 +1,55 @@ +import { createEnv, Env } from "@executor/env"; + +const sharedShape = { + NODE_ENV: Env.literalOr( + "NODE_ENV", + "development", + "development", + "test", + "production", + ), +}; + +const serverShape = { + DATABASE_URL: Env.stringOr("DATABASE_URL", ""), + PGLITE_DATA_DIR: Env.stringOr("PGLITE_DATA_DIR", ".pglite"), + ENCRYPTION_KEY: Env.stringOr( + "ENCRYPTION_KEY", + "local-dev-encryption-key", + ), + WORKOS_API_KEY: Env.string("WORKOS_API_KEY"), + WORKOS_CLIENT_ID: Env.string("WORKOS_CLIENT_ID"), + WORKOS_COOKIE_PASSWORD: Env.string("WORKOS_COOKIE_PASSWORD"), +}; + +type SharedEnv = Readonly<{ + NODE_ENV: "development" | "test" | "production"; +}>; + +type ServerEnv = SharedEnv & Readonly<{ + DATABASE_URL: string; + PGLITE_DATA_DIR: string; + ENCRYPTION_KEY: string; + WORKOS_API_KEY: string; + WORKOS_CLIENT_ID: string; + WORKOS_COOKIE_PASSWORD: string; +}>; + +type WebEnv = Readonly>; + +export const shared = createEnv(sharedShape, { + runtimeEnv: process.env, + emptyStringAsUndefined: true, +}) as SharedEnv; + +export const web = createEnv({}, { + prefix: "PUBLIC_", + runtimeEnv: process.env, + emptyStringAsUndefined: true, +}) as WebEnv; + +export const server = createEnv(serverShape, { + extends: [shared], + runtimeEnv: process.env, + emptyStringAsUndefined: true, +}) as ServerEnv; diff --git a/apps/cloud/src/services/db.ts b/apps/cloud/src/services/db.ts index b162d23b3a..c954ada5af 100644 --- a/apps/cloud/src/services/db.ts +++ b/apps/cloud/src/services/db.ts @@ -7,6 +7,7 @@ import { resolve } from "node:path"; import * as sharedSchema from "@executor/storage-postgres/schema"; import * as cloudSchema from "./schema"; import type { DrizzleDb } from "@executor/storage-postgres"; +import { server } from "../env"; const schema = { ...sharedSchema, ...cloudSchema }; @@ -23,11 +24,11 @@ type DbResource = { }; const createDbResource = async (): Promise => { - if (process.env.DATABASE_URL) { + if (server.DATABASE_URL) { const { drizzle } = await import("drizzle-orm/node-postgres"); const { migrate } = await import("drizzle-orm/node-postgres/migrator"); const { Pool } = await import("pg"); - const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + const pool = new Pool({ connectionString: server.DATABASE_URL }); const db = drizzle(pool, { schema }) as DrizzleDb; await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR }); return { @@ -39,7 +40,7 @@ const createDbResource = async (): Promise => { const { PGlite } = await import("@electric-sql/pglite"); const { drizzle } = await import("drizzle-orm/pglite"); const { migrate } = await import("drizzle-orm/pglite/migrator"); - const dataDir = process.env.PGLITE_DATA_DIR ?? ".pglite"; + const dataDir = server.PGLITE_DATA_DIR; const client = new PGlite(dataDir); const db = drizzle(client, { schema }) as DrizzleDb; await migrate(db, { migrationsFolder: MIGRATIONS_DIR }); diff --git a/apps/cloud/src/web/auth.tsx b/apps/cloud/src/web/auth.tsx index 9de98bdbe5..e746966a97 100644 --- a/apps/cloud/src/web/auth.tsx +++ b/apps/cloud/src/web/auth.tsx @@ -23,9 +23,10 @@ type AuthTeam = { // Auth atom — typed query against CloudAuthApi // --------------------------------------------------------------------------- -export const authAtom = CloudApiClient.query("cloudAuth", "me", { - timeToLive: "5 minutes", -}); +export const authAtom = + CloudApiClient.query("cloudAuth", "me", { + timeToLive: "5 minutes", + }); // --------------------------------------------------------------------------- // Provider + hook diff --git a/apps/cloud/tsconfig.json b/apps/cloud/tsconfig.json index e9713c832b..7d2b670967 100644 --- a/apps/cloud/tsconfig.json +++ b/apps/cloud/tsconfig.json @@ -8,8 +8,8 @@ "skipLibCheck": true, "outDir": "dist", "rootDir": ".", - "declaration": true, - "declarationMap": true, + "declaration": false, + "declarationMap": false, "sourceMap": true, "jsx": "react-jsx", "plugins": [ diff --git a/apps/cloud/vite.config.ts b/apps/cloud/vite.config.ts index 8de032c45d..17162a3b6b 100644 --- a/apps/cloud/vite.config.ts +++ b/apps/cloud/vite.config.ts @@ -2,10 +2,24 @@ import { defineConfig } from "vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import { createEnv, Env } from "@executor/env"; + +const server = { + PORT: Env.numberOr("PORT", 5173), +}; + +type ViteEnv = Readonly<{ + PORT: number; +}>; + +const viteEnv = createEnv(server, { + runtimeEnv: process.env, + emptyStringAsUndefined: true, +}) as ViteEnv; export default defineConfig({ server: { - port: parseInt(process.env.PORT ?? "5173", 10), + port: viteEnv.PORT, host: "127.0.0.1", }, resolve: { tsconfigPaths: true }, diff --git a/bun.lock b/bun.lock index 8d5efcfab1..1dd59e990b 100644 --- a/bun.lock +++ b/bun.lock @@ -47,6 +47,7 @@ "@effect-atom/atom-react": "^0.5.0", "@effect/platform": "catalog:", "@executor/api": "workspace:*", + "@executor/env": "workspace:*", "@executor/execution": "workspace:*", "@executor/plugin-google-discovery": "workspace:*", "@executor/plugin-graphql": "workspace:*",