Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# Cloudflare
.wrangler/
.alchemy/
personal-notes/
*.har.executor
executor.har
Expand Down
5 changes: 1 addition & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.native-preview.tsdk": "node_modules/@typescript/native-preview",
"typescript.experimental.useTsgo": true,
"js/ts.experimental.useTsgo": true
"typescript.enablePromptUseWorkspaceTsdk": true
}
14 changes: 14 additions & 0 deletions apps/cloud/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import { cloudStack } from "./infra/stack";

const appDir = new URL(".", import.meta.url).pathname;

export default Alchemy.Stack(
"ExecutorCloud",
{
providers: Cloudflare.providers(),
state: Cloudflare.state(),
},
cloudStack(appDir),
);
181 changes: 181 additions & 0 deletions apps/cloud/infra/stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as Match from "effect/Match";
import * as Redacted from "effect/Redacted";
import McpSessionDO from "../src/mcp-session";

const DEFAULT_LOCAL_DATABASE_URL = "postgresql://postgres:postgres@127.0.0.1:5433/postgres";

export const cloudWorker = (appDir: string, hyperdrive: Cloudflare.Hyperdrive) =>
Cloudflare.Vite("Cloud", {
rootDir: appDir,
name: "executor-cloud",
compatibility: {
date: "2025-04-01",
flags: ["nodejs_compat"],
},
domain: "executor.sh",
limits: {
cpuMs: 30000,
},
observability: { enabled: true },
placement: { region: "aws:us-east-1" },
env: {
APP_URL: process.env.APP_URL ?? publicSiteUrl(),
...optionalTextEnv("AUTUMN_SECRET_KEY"),
...optionalTextEnv("AXIOM_DATASET"),
...optionalTextEnv("AXIOM_TOKEN"),
...optionalTextEnv("AXIOM_TRACES_SAMPLE_RATIO"),
...optionalTextEnv("AXIOM_TRACES_URL"),
...optionalTextEnv("ENCRYPTION_KEY"),
...optionalTextEnv("MCP_AUTHKIT_DOMAIN"),
...optionalTextEnv("MCP_RESOURCE_ORIGIN"),
...optionalTextEnv("SENTRY_DSN"),
...optionalTextEnv("SLACK_BOT_TOKEN"),
...optionalTextEnv("TURNSTILE_SECRET_KEY"),
VITE_PUBLIC_SITE_URL: publicSiteUrl(),
...optionalTextEnv("VITE_PUBLIC_SENTRY_DSN"),
VITE_PUBLIC_POSTHOG_KEY: "phc_nNLrNMALpRsfrEkZovUkfMxYbcJvHnsJHeoSPavprgLL",
...optionalTextEnv("VITE_PUBLIC_POSTHOG_HOST"),
...optionalTextEnv("VITE_PUBLIC_TURNSTILE_SITEKEY"),
WORKOS_API_KEY: requiredText("WORKOS_API_KEY"),
...optionalTextEnv("WORKOS_CLAIM_TOKEN"),
WORKOS_CLIENT_ID: requiredText("WORKOS_CLIENT_ID"),
WORKOS_COOKIE_PASSWORD: requiredText("WORKOS_COOKIE_PASSWORD"),
...localDirectDatabaseEnv(),
},
bindings: {
MCP_SESSION: McpSessionDO,
LOADER: Cloudflare.DynamicWorkerLoader("LOADER"),
MARKETING: Cloudflare.ServiceBinding("executor-marketing"),
HYPERDRIVE: hyperdrive,
},
memo: {
include: [
"alchemy.run.ts",
"executor.config.ts",
"index.html",
"package.json",
"scripts/**",
"src/**",
"vite.config.ts",
"../../package.json",
"../../bun.lock",
"../../packages/**/package.json",
"../../packages/**/src/**",
],
},
});

export const cloudStack = (appDir: string) =>
Effect.gen(function* () {
const origin = yield* hyperdriveOriginFromUrl(
Redacted.make(process.env.DATABASE_URL ?? DEFAULT_LOCAL_DATABASE_URL),
);
const hyperdrive = yield* Cloudflare.Hyperdrive("HYPERDRIVE", {
name: "planetscale-executor-main-axub",
origin,
dev: {
scheme: "postgresql",
host: "127.0.0.1",
port: 5433,
database: "postgres",
user: "postgres",
password: Redacted.make("postgres"),
sslmode: "prefer",
},
});

const worker = yield* cloudWorker(appDir, hyperdrive);

return {
workerName: worker.workerName,
url: worker.url,
};
});

const hyperdriveOriginFromUrl = (
databaseUrl: Redacted.Redacted<string>,
): Effect.Effect<Cloudflare.HyperdrivePublicOrigin> =>
Effect.gen(function* () {
const url = new URL(Redacted.value(databaseUrl));
const scheme = yield* parseHyperdriveScheme(url.protocol);
return {
scheme,
host: url.hostname || "127.0.0.1",
port: url.port ? Number(url.port) : undefined,
database: decodeURIComponent(url.pathname.replace(/^\/+/, "")) || defaultDatabase(scheme),
user: decodeURIComponent(url.username || defaultUser(scheme)),
password: Redacted.make(decodeURIComponent(url.password || defaultPassword(scheme))),
};
});

const parseHyperdriveScheme = (protocol: string): Effect.Effect<Cloudflare.HyperdriveScheme> =>
Effect.gen(function* () {
const normalized = protocol.replace(/:$/, "");
if (!isHyperdriveScheme(normalized)) {
// oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: Alchemy stack bodies cannot carry typed user-input validation errors
return yield* Effect.die(`Unsupported Hyperdrive protocol: ${normalized}`);
}
return hyperdriveScheme(normalized);
});

const isHyperdriveScheme = (value: string): value is Cloudflare.HyperdriveScheme =>
Match.value(value).pipe(
Match.when("mysql", () => true),
Match.when("postgres", () => true),
Match.when("postgresql", () => true),
Match.orElse(() => false),
);

const hyperdriveScheme = (protocol: Cloudflare.HyperdriveScheme): Cloudflare.HyperdriveScheme =>
Match.value(protocol).pipe(
Match.when("mysql", () => "mysql" as const),
Match.when("postgres", () => "postgres" as const),
Match.when("postgresql", () => "postgresql" as const),
Match.exhaustive,
);

const defaultDatabase = (scheme: Cloudflare.HyperdriveScheme): string =>
Match.value(scheme).pipe(
Match.when("mysql", () => "mysql"),
Match.when("postgres", () => "postgres"),
Match.when("postgresql", () => "postgres"),
Match.exhaustive,
);

const defaultUser = (scheme: Cloudflare.HyperdriveScheme): string =>
Match.value(scheme).pipe(
Match.when("mysql", () => "root"),
Match.when("postgres", () => "postgres"),
Match.when("postgresql", () => "postgres"),
Match.exhaustive,
);

const defaultPassword = (scheme: Cloudflare.HyperdriveScheme): string =>
Match.value(scheme).pipe(
Match.when("mysql", () => ""),
Match.when("postgres", () => "postgres"),
Match.when("postgresql", () => "postgres"),
Match.exhaustive,
);

const requiredText = (name: string): string => process.env[name] ?? "";

const optionalText = (name: string): string | undefined => process.env[name];

const publicSiteUrl = (): string =>
process.env.VITE_PUBLIC_SITE_URL ?? process.env.PORTLESS_URL ?? "https://executor.sh";

const optionalTextEnv = (name: string): Record<string, string> => {
const value = optionalText(name);
return value === undefined ? {} : { [name]: value };
};

const localDirectDatabaseEnv = (): Record<string, string> =>
process.env.EXECUTOR_DIRECT_DATABASE_URL === "true"
? {
EXECUTOR_DIRECT_DATABASE_URL: "true",
DATABASE_URL: DEFAULT_LOCAL_DATABASE_URL,
}
: {};
27 changes: 8 additions & 19 deletions apps/cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"private": true,
"type": "module",
"scripts": {
"dev": "bun run dev:proxy && concurrently -n db,vite -c blue,green \"bun run dev:db\" \"bun run dev:vite\"",
"dev": "bun run dev:proxy && concurrently -n db,alchemy -c blue,green \"bun run dev:db\" \"bun run dev:alchemy\"",
"dev:proxy": "portless proxy start --multiplex --shared-port --port 5394 || (portless proxy stop -p 5394 && portless proxy start --multiplex --shared-port --port 5394)",
"dev:db": "bun run scripts/dev-db.ts",
"dev:vite": "EXECUTOR_DIRECT_DATABASE_URL=true CLOUDFLARE_INCLUDE_PROCESS_ENV=true op run --env-file=.env.op -- portless --name executor-cloud vite dev",
"dev:alchemy": "EXECUTOR_DIRECT_DATABASE_URL=true CLOUDFLARE_INCLUDE_PROCESS_ENV=true op run --env-file=.env.op -- portless --name executor-cloud bun alchemy dev",
"db:schema": "node --import jiti/register ../../packages/core/cli/src/index.ts generate --config ./executor.config.ts --output ./src/services/executor-schema.ts",
"db:generate": "drizzle-kit generate",
"db:studio": "drizzle-kit studio",
Expand All @@ -16,18 +16,18 @@
"db:migrate:dev": "op run --env-file=.env.op -- bun --bun ../../node_modules/.bun/node_modules/drizzle-kit/bin.cjs migrate",
"build": "node scripts/build.mjs",
"preview": "vite preview",
"deploy": "op run --env-file=.env.production -- bun run build && op run --env-file=.env.production -- wrangler deploy",
"cf-typegen": "wrangler types",
"plan": "op run --env-file=.env.production -- bun alchemy plan --profile prod --stage prod",
"deploy": "op run --env-file=.env.production -- bun alchemy deploy --profile prod --stage prod",
"deploy:adopt": "op run --env-file=.env.production -- bun alchemy deploy --profile prod --stage prod --adopt",
"destroy": "op run --env-file=.env.production -- bun alchemy destroy --profile prod --stage prod",
"typecheck": "tsgo --noEmit",
"test": "node ../../node_modules/vitest/vitest.mjs run && node ../../node_modules/vitest/vitest.mjs run --config vitest.node.config.ts",
"test:watch": "node ../../node_modules/vitest/vitest.mjs",
"test:node": "node ../../node_modules/vitest/vitest.mjs run --config vitest.node.config.ts",
"typecheck:slow": "tsc --noEmit"
},
"dependencies": {
"@cloudflare/vite-plugin": "^1.31.1",
"@effect/atom-react": "catalog:",
"@effect/opentelemetry": "catalog:",
"@executor-js/api": "workspace:*",
"@executor-js/execution": "workspace:*",
"@executor-js/host-mcp": "workspace:*",
Expand All @@ -42,22 +42,13 @@
"@executor-js/storage-core": "workspace:*",
"@executor-js/storage-postgres": "workspace:*",
"@executor-js/vite-plugin": "workspace:*",
"@microlabs/otel-cf-workers": "^1.0.0-rc.52",
"@modelcontextprotocol/sdk": "^1.29.0",
"@opentelemetry/api": "~1.9.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
"@opentelemetry/resources": "^2.6.1",
"@opentelemetry/sdk-logs": "^0.214.0",
"@opentelemetry/sdk-trace-base": "^2.6.1",
"@opentelemetry/sdk-trace-web": "^2.6.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@sentry/cloudflare": "^10.48.0",
"@sentry/react": "^10.48.0",
"@tanstack/react-router": "catalog:",
"@tanstack/react-start": "catalog:",
"@workos-inc/node": "^8.11.1",
"agents": "^0.10.0",
"alchemy": "https://registry.npmjs.org/@rhyssul/alchemy/-/alchemy-2.0.0-beta.36-rhyssul.11.tgz",
"autumn-js": "^1.2.8",
"drizzle-orm": "catalog:",
"effect": "catalog:",
Expand All @@ -69,7 +60,6 @@
"sonner": "^2.0.7"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.15.0",
"@cloudflare/workers-types": "^4.20250620.0",
"@effect/platform-node": "catalog:",
"@effect/vitest": "catalog:",
Expand All @@ -86,7 +76,6 @@
"jiti": "^2.6.1",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "^4.1.5",
"wrangler": "^4.81.0"
"vitest": "^4.1.5"
}
}
3 changes: 3 additions & 0 deletions apps/cloud/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

import { spawnSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import { rmSync } from "node:fs";

if (!process.env.VITE_PUBLIC_ANALYTICS_PATH) {
process.env.VITE_PUBLIC_ANALYTICS_PATH = randomBytes(4).toString("hex");
}
console.log(`[build] VITE_PUBLIC_ANALYTICS_PATH=${process.env.VITE_PUBLIC_ANALYTICS_PATH}`);

rmSync(new URL("../dist/", import.meta.url), { force: true, recursive: true });

const steps = ["turbo run build --filter @executor-js/vite-plugin", "vite build"];

for (const step of steps) {
Expand Down
6 changes: 3 additions & 3 deletions apps/cloud/scripts/test-globalsetup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ---------------------------------------------------------------------------
// Vitest globalSetup — starts an in-process PGlite socket server so tests
// running in the Cloudflare Workers runtime can connect to a real Postgres
// via postgres.js. Port must match DATABASE_URL in wrangler.test.jsonc.
// Vitest globalSetup — starts an in-process PGlite socket server so tests can
// connect to a real Postgres endpoint via postgres.js. Port must match
// DATABASE_URL in the Vitest configs.
// ---------------------------------------------------------------------------

import { PGlite } from "@electric-sql/pglite";
Expand Down
19 changes: 13 additions & 6 deletions apps/cloud/src/api.request-scope.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
// for `acquireRelease`.
// ---------------------------------------------------------------------------

import * as Cloudflare from "alchemy/Cloudflare/Workers/Runtime";
import { describe, it, expect } from "@effect/vitest";
import { Context, Effect, Layer } from "effect";
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http";
Expand Down Expand Up @@ -166,6 +167,8 @@ describe("makeApiLive (prod handler factory) request scoping", () => {
// Wrap the real per-request layer with an `acquireRelease` counter.
// `requestScopedMiddleware` calls `Layer.build` per request, so this
// counter increments per request iff the wiring is correct.
const workerEnvLive = Cloudflare.WorkerEnvironment.layer(process.env);
const requestScopedLive = RequestScopedServicesLive.pipe(Layer.provide(workerEnvLive));
const trackedRsLive = Layer.effectDiscard(
Effect.acquireRelease(
Effect.sync(() => {
Expand All @@ -176,18 +179,22 @@ describe("makeApiLive (prod handler factory) request scoping", () => {
counts.releases += 1;
}),
),
).pipe(Layer.provideMerge(RequestScopedServicesLive));
).pipe(Layer.provideMerge(requestScopedLive));

const handler = HttpRouter.toWebHandler(makeApiLive(trackedRsLive), {
disableLogger: true,
}).handler;
const handler = HttpRouter.toWebHandler(
makeApiLive(trackedRsLive).pipe(Layer.provide(workerEnvLive)),
{
disableLogger: true,
},
).handler;
const workerEnvContext = Context.make(Cloudflare.WorkerEnvironment, process.env);

// Hit a protected route. ExecutionStackMiddleware short-circuits with
// 403 (no session cookie) but not before `requestScopedMiddleware`
// has built the per-request layer. We don't care about the response —
// only that the layer was built once per request.
await handler(new Request("http://test.local/scope"));
await handler(new Request("http://test.local/scope"));
await handler(new Request("http://test.local/scope"), workerEnvContext);
await handler(new Request("http://test.local/scope"), workerEnvContext);

expect(counts.acquires).toBe(2);
expect(counts.releases).toBe(2);
Expand Down
24 changes: 22 additions & 2 deletions apps/cloud/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import * as Cloudflare from "alchemy/Cloudflare/Workers/Runtime";
import { Context, Layer } from "effect";
import { HttpRouter } from "effect/unstable/http";

import { ApiLive } from "./api/router";
import { RequestScopedServicesLive } from "./api/layers";
import { makeApiLive } from "./api/router";

export const handleApiRequest = HttpRouter.toWebHandler(ApiLive).handler;
const handlers = new WeakMap<
Env,
(request: Request, context: Context.Context<Cloudflare.WorkerEnvironment>) => Promise<Response>
>();

export const handleApiRequest = (request: Request, env: Env): Promise<Response> => {
const existing = handlers.get(env);
const context = Context.make(Cloudflare.WorkerEnvironment, env);
if (existing) return existing(request, context);

const workerEnvLive = Cloudflare.WorkerEnvironment.layer(env);
const requestScopedLive = RequestScopedServicesLive.pipe(Layer.provide(workerEnvLive));
const handler = HttpRouter.toWebHandler(
makeApiLive(requestScopedLive).pipe(Layer.provide(workerEnvLive)),
).handler;
handlers.set(env, handler);
return handler(request, context);
};
Loading