History

feat(cloud): use @executor/env for env access (#100)

Rhys Sullivan committed 7 months ago

Commit b5f705e2 parents 004ac0d 1b950e410 files · +91 −15Browse files

Unified · Split

10 files
  1. apps/cloud/package.json +1 −0
  2. apps/cloud/src/api.ts +3 −2
  3. apps/cloud/src/auth/handlers.ts +2 −1
  4. apps/cloud/src/auth/workos.ts +4 −3
  5. apps/cloud/src/env.ts +55 −0
  6. apps/cloud/src/services/db.ts +4 −3
  7. apps/cloud/src/web/auth.tsx +4 −3
  8. apps/cloud/tsconfig.json +2 −2
  9. apps/cloud/vite.config.ts +15 −1
  10. bun.lock +1 −0
apps/cloud/package.json+1−0
@@ -8,6 +8,7 @@ · 7 unmodified lines
8 "@effect-atom/atom-react": "^0.5.0",8 "@effect-atom/atom-react": "^0.5.0",
9 "@effect/platform": "catalog:",9 "@effect/platform": "catalog:",
10 "@executor/api": "workspace:*",10 "@executor/api": "workspace:*",
11 "@executor/env": "workspace:*",
11 "@executor/execution": "workspace:*",12 "@executor/execution": "workspace:*",
12 "@executor/plugin-google-discovery": "workspace:*",13 "@executor/plugin-google-discovery": "workspace:*",
13 "@executor/plugin-graphql": "workspace:*",14 "@executor/plugin-graphql": "workspace:*",
apps/cloud/src/api.ts+3−2
@@ -31,6 +31,7 @@ import { CloudAuthHandlers, CloudAuthPublicHandlers } from "./auth/handlers"; · 30 unmodified lines
31import { WorkOSAuth } from "./auth/workos";31import { WorkOSAuth } from "./auth/workos";
32import { DbService } from "./services/db";32import { DbService } from "./services/db";
33import { createTeamExecutor } from "./services/executor";33import { createTeamExecutor } from "./services/executor";
34import { server } from "./env";
34 35
35const ProtectedCloudApi = addGroup(OpenApiGroup)36const ProtectedCloudApi = addGroup(OpenApiGroup)
36 .add(McpGroup)37 .add(McpGroup)
@@ -104,7 +105,7 @@ const COOKIE_OPTIONS = { · 67 unmodified lines
104 httpOnly: true,105 httpOnly: true,
105 sameSite: "lax" as const,106 sameSite: "lax" as const,
106 maxAge: 60 * 60 * 24 * 7,107 maxAge: 60 * 60 * 24 * 7,
107 secure: process.env.NODE_ENV === "production",108 secure: server.NODE_ENV === "production",
108};109};
109 110
110const resolveAuth = (request: Request) =>111const resolveAuth = (request: Request) =>
@@ -151,7 +152,7 @@ const resolveExecutor = (teamId: string) => · 40 unmodified lines
151 const users = yield* UserStoreService;152 const users = yield* UserStoreService;
152 const team = yield* users.use((store) => store.getTeam(teamId));153 const team = yield* users.use((store) => store.getTeam(teamId));
153 const teamName = team?.name ?? "Unknown Team";154 const teamName = team?.name ?? "Unknown Team";
154 const encryptionKey = process.env.ENCRYPTION_KEY ?? "local-dev-encryption-key";155 const encryptionKey = server.ENCRYPTION_KEY;
155 return yield* createTeamExecutor(teamId, teamName, encryptionKey);156 return yield* createTeamExecutor(teamId, teamName, encryptionKey);
156 });157 });
157 158
apps/cloud/src/auth/handlers.ts+2−1
@@ -6,13 +6,14 @@ import { addGroup } from "@executor/api"; · 5 unmodified lines
6import { AUTH_PATHS, CloudAuthApi, CloudAuthPublicApi } from "./api";6import { AUTH_PATHS, CloudAuthApi, CloudAuthPublicApi } from "./api";
7import { AuthContext, UserStoreService } from "./context";7import { AuthContext, UserStoreService } from "./context";
8import { WorkOSAuth } from "./workos";8import { WorkOSAuth } from "./workos";
9import { server } from "../env";
9 10
10const COOKIE_OPTIONS = {11const COOKIE_OPTIONS = {
11 path: "/",12 path: "/",
12 httpOnly: true,13 httpOnly: true,
13 sameSite: "lax" as const,14 sameSite: "lax" as const,
14 maxAge: 60 * 60 * 24 * 7,15 maxAge: 60 * 60 * 24 * 7,
15 secure: process.env.NODE_ENV === "production",16 secure: server.NODE_ENV === "production",
16};17};
17 18
18// ---------------------------------------------------------------------------19// ---------------------------------------------------------------------------
apps/cloud/src/auth/workos.ts+4−3
@@ -5,6 +5,7 @@ · 4 unmodified lines
5import { Context, Effect, Layer } from "effect";5import { Context, Effect, Layer } from "effect";
6import { WorkOS } from "@workos-inc/node";6import { WorkOS } from "@workos-inc/node";
7import { WorkOSError } from "./errors";7import { WorkOSError } from "./errors";
8import { server } from "../env";
8 9
9const COOKIE_NAME = "wos-session";10const COOKIE_NAME = "wos-session";
10 11
@@ -14,9 +15,9 @@ const COOKIE_NAME = "wos-session"; · 3 unmodified lines
14 15
15 16
16const make = Effect.gen(function* () {17const make = Effect.gen(function* () {
17 const apiKey = process.env.WORKOS_API_KEY!;18 const apiKey = server.WORKOS_API_KEY;
18 const clientId = process.env.WORKOS_CLIENT_ID!;19 const clientId = server.WORKOS_CLIENT_ID;
19 const cookiePassword = process.env.WORKOS_COOKIE_PASSWORD!;20 const cookiePassword = server.WORKOS_COOKIE_PASSWORD;
20 21
21 if (!cookiePassword || cookiePassword.length < 32) {22 if (!cookiePassword || cookiePassword.length < 32) {
22 return yield* Effect.die(new Error("WORKOS_COOKIE_PASSWORD must be at least 32 characters"));23 return yield* Effect.die(new Error("WORKOS_COOKIE_PASSWORD must be at least 32 characters"));
apps/cloud/src/env.ts+55−0
@@ -0,0 +1,55 @@
1import { createEnv, Env } from "@executor/env";
2
3const sharedShape = {
4 NODE_ENV: Env.literalOr(
5 "NODE_ENV",
6 "development",
7 "development",
8 "test",
9 "production",
10 ),
11};
12
13const serverShape = {
14 DATABASE_URL: Env.stringOr("DATABASE_URL", ""),
15 PGLITE_DATA_DIR: Env.stringOr("PGLITE_DATA_DIR", ".pglite"),
16 ENCRYPTION_KEY: Env.stringOr(
17 "ENCRYPTION_KEY",
18 "local-dev-encryption-key",
19 ),
20 WORKOS_API_KEY: Env.string("WORKOS_API_KEY"),
21 WORKOS_CLIENT_ID: Env.string("WORKOS_CLIENT_ID"),
22 WORKOS_COOKIE_PASSWORD: Env.string("WORKOS_COOKIE_PASSWORD"),
23};
24
25type SharedEnv = Readonly<{
26 NODE_ENV: "development" | "test" | "production";
27}>;
28
29type ServerEnv = SharedEnv & Readonly<{
30 DATABASE_URL: string;
31 PGLITE_DATA_DIR: string;
32 ENCRYPTION_KEY: string;
33 WORKOS_API_KEY: string;
34 WORKOS_CLIENT_ID: string;
35 WORKOS_COOKIE_PASSWORD: string;
36}>;
37
38type WebEnv = Readonly<Record<string, never>>;
39
40export const shared = createEnv(sharedShape, {
41 runtimeEnv: process.env,
42 emptyStringAsUndefined: true,
43}) as SharedEnv;
44
45export const web = createEnv({}, {
46 prefix: "PUBLIC_",
47 runtimeEnv: process.env,
48 emptyStringAsUndefined: true,
49}) as WebEnv;
50
51export const server = createEnv(serverShape, {
52 extends: [shared],
53 runtimeEnv: process.env,
54 emptyStringAsUndefined: true,
55}) as ServerEnv;
apps/cloud/src/services/db.ts+4−3
@@ -7,6 +7,7 @@ import { resolve } from "node:path"; · 6 unmodified lines
7import * as sharedSchema from "@executor/storage-postgres/schema";7import * as sharedSchema from "@executor/storage-postgres/schema";
8import * as cloudSchema from "./schema";8import * as cloudSchema from "./schema";
9import type { DrizzleDb } from "@executor/storage-postgres";9import type { DrizzleDb } from "@executor/storage-postgres";
10import { server } from "../env";
10 11
11const schema = { ...sharedSchema, ...cloudSchema };12const schema = { ...sharedSchema, ...cloudSchema };
12 13
@@ -23,11 +24,11 @@ type DbResource = { · 10 unmodified lines
23};24};
24 25
25const createDbResource = async (): Promise<DbResource> => {26const createDbResource = async (): Promise<DbResource> => {
26 if (process.env.DATABASE_URL) {27 if (server.DATABASE_URL) {
27 const { drizzle } = await import("drizzle-orm/node-postgres");28 const { drizzle } = await import("drizzle-orm/node-postgres");
28 const { migrate } = await import("drizzle-orm/node-postgres/migrator");29 const { migrate } = await import("drizzle-orm/node-postgres/migrator");
29 const { Pool } = await import("pg");30 const { Pool } = await import("pg");
30 const pool = new Pool({ connectionString: process.env.DATABASE_URL });31 const pool = new Pool({ connectionString: server.DATABASE_URL });
31 const db = drizzle(pool, { schema }) as DrizzleDb;32 const db = drizzle(pool, { schema }) as DrizzleDb;
32 await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR });33 await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR });
33 return {34 return {
@@ -39,7 +40,7 @@ const createDbResource = async (): Promise<DbResource> => { · 5 unmodified lines
39 const { PGlite } = await import("@electric-sql/pglite");40 const { PGlite } = await import("@electric-sql/pglite");
40 const { drizzle } = await import("drizzle-orm/pglite");41 const { drizzle } = await import("drizzle-orm/pglite");
41 const { migrate } = await import("drizzle-orm/pglite/migrator");42 const { migrate } = await import("drizzle-orm/pglite/migrator");
42 const dataDir = process.env.PGLITE_DATA_DIR ?? ".pglite";43 const dataDir = server.PGLITE_DATA_DIR;
43 const client = new PGlite(dataDir);44 const client = new PGlite(dataDir);
44 const db = drizzle(client, { schema }) as DrizzleDb;45 const db = drizzle(client, { schema }) as DrizzleDb;
45 await migrate(db, { migrationsFolder: MIGRATIONS_DIR });46 await migrate(db, { migrationsFolder: MIGRATIONS_DIR });
apps/cloud/src/web/auth.tsx+4−3
@@ -23,9 +23,10 @@ type AuthTeam = { · 22 unmodified lines
23// Auth atom — typed query against CloudAuthApi23// Auth atom — typed query against CloudAuthApi
24// ---------------------------------------------------------------------------24// ---------------------------------------------------------------------------
25 25
26export const authAtom = CloudApiClient.query("cloudAuth", "me", {26export const authAtom =
27 timeToLive: "5 minutes",27 CloudApiClient.query("cloudAuth", "me", {
28});28 timeToLive: "5 minutes",
29 });
29 30
30// ---------------------------------------------------------------------------31// ---------------------------------------------------------------------------
31// Provider + hook32// Provider + hook
apps/cloud/tsconfig.json+2−2
@@ -8,8 +8,8 @@ · 7 unmodified lines
8 "skipLibCheck": true,8 "skipLibCheck": true,
9 "outDir": "dist",9 "outDir": "dist",
10 "rootDir": ".",10 "rootDir": ".",
11 "declaration": true,11 "declaration": false,
12 "declarationMap": true,12 "declarationMap": false,
13 "sourceMap": true,13 "sourceMap": true,
14 "jsx": "react-jsx",14 "jsx": "react-jsx",
15 "plugins": [15 "plugins": [
apps/cloud/vite.config.ts+15−1
@@ -2,10 +2,24 @@ import { defineConfig } from "vite"; · 1 unmodified line
2import { tanstackStart } from "@tanstack/react-start/plugin/vite";2import { tanstackStart } from "@tanstack/react-start/plugin/vite";
3import react from "@vitejs/plugin-react";3import react from "@vitejs/plugin-react";
4import tailwindcss from "@tailwindcss/vite";4import tailwindcss from "@tailwindcss/vite";
5import { createEnv, Env } from "@executor/env";
6
7const server = {
8 PORT: Env.numberOr("PORT", 5173),
9};
10
11type ViteEnv = Readonly<{
12 PORT: number;
13}>;
14
15const viteEnv = createEnv(server, {
16 runtimeEnv: process.env,
17 emptyStringAsUndefined: true,
18}) as ViteEnv;
5 19
6export default defineConfig({20export default defineConfig({
7 server: {21 server: {
8 port: parseInt(process.env.PORT ?? "5173", 10),22 port: viteEnv.PORT,
9 host: "127.0.0.1",23 host: "127.0.0.1",
10 },24 },
11 resolve: { tsconfigPaths: true },25 resolve: { tsconfigPaths: true },
bun.lock+1−0
@@ -47,6 +47,7 @@ · 46 unmodified lines
47 "@effect-atom/atom-react": "^0.5.0",47 "@effect-atom/atom-react": "^0.5.0",
48 "@effect/platform": "catalog:",48 "@effect/platform": "catalog:",
49 "@executor/api": "workspace:*",49 "@executor/api": "workspace:*",
50 "@executor/env": "workspace:*",
50 "@executor/execution": "workspace:*",51 "@executor/execution": "workspace:*",
51 "@executor/plugin-google-discovery": "workspace:*",52 "@executor/plugin-google-discovery": "workspace:*",
52 "@executor/plugin-graphql": "workspace:*",53 "@executor/plugin-graphql": "workspace:*",