// The cloud boot recipe — ONE definition shared by the vitest globalsetup // (ephemeral) and the dev CLI (persistent): WorkOS + Autumn EMULATORS in this // process plus the app's own dev stack (PGlite dev-db + vite dev) pointed at // them. The app runs its REAL auth/billing code — real SDKs, real // sealed-session crypto, real JWKS — against emulated services. import { rmSync } from "node:fs"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; // Vendored fork import (same pattern as mcporter). import { createEmulator } from "@executor-js/emulate"; import { bootProcesses, waitForBoot, waitForHttp } from "./boot"; import { AUTUMN_PLAN_SEED } from "./autumn-plans"; import { E2E_EXECUTION_RATE_LIMIT, E2E_EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS, } from "./execution-limits"; export const cloudDir = fileURLToPath(new URL("../../apps/cloud/", import.meta.url)); export interface CloudBootOptions { readonly cloudPort: number; readonly dbPort: number; readonly workosPort: number; readonly autumnPort: number; readonly workosClientId: string; readonly cookiePassword: string; /** The URL the app advertises (VITE_PUBLIC_SITE_URL, MCP resource origin). */ readonly publicUrl: string; /** * The WorkOS origin the BROWSER must reach (authorize page redirects). * Defaults to the emulator's loopback URL; set it when a proxy (e.g. * `tailscale serve` HTTPS) fronts the emulator — the app's auth cookies * are Secure, so off-localhost access needs https on both sides. */ readonly workosPublicUrl?: string; /** vite --host. Default 127.0.0.1. */ readonly host?: string; /** Wipe the dev DB before boot (hermetic). Default true. */ readonly fresh?: boolean; readonly logFile?: string; /** Extra env for the app's dev stack (e.g. the suite's OTLP exporter). */ readonly extraEnv?: Record; } export interface CloudBooted { readonly teardown: () => Promise; readonly pids: ReadonlyArray; readonly workosUrl: string; readonly autumnUrl: string; } export const bootCloud = async (options: CloudBootOptions): Promise => { // Fresh dev DB per boot — the WorkOS emulator mints org ids from a // per-process counter, so a persisted DB from a previous invocation // collides with the new boot's ids (identities land in polluted orgs / // org creation 500s). const dbPath = resolve(cloudDir, ".e2e-stub-db"); if (options.fresh ?? true) rmSync(dbPath, { recursive: true, force: true }); // Fresh worker state per boot, for the same reason as the DB: miniflare // persists the Workers Cache API under .wrangler/state across dev-server // runs, and the JWKS L2 cache (auth/jwks-cache.ts) lives there keyed by // URL. The WorkOS emulator binds this checkout's stable port block, so run // N+1's dev server serves run N's cached JWKS (stale-while-revalidate) // against a fresh emulator's keys — every session verify then fails // signature, falls into single-use refresh, and concurrent requests race // each other's rotation into 401s. One run poisons the next. if (options.fresh ?? true) { rmSync(resolve(cloudDir, ".wrangler", "state"), { recursive: true, force: true }); } // MCP access tokens minted by the emulator's OAuth server must carry the // app's client id as audience (what the resource server verifies). process.env.EMULATE_WORKOS_AUDIENCE = options.workosClientId; const workos = await createEmulator({ service: "workos", port: options.workosPort, ...(options.workosPublicUrl ? { baseUrl: options.workosPublicUrl } : {}), }); const autumn = await createEmulator({ service: "autumn", port: options.autumnPort, // Seed the plan catalog so the billing UI (plans, eligibility, trial // checkout) has real plans to render. Derived from autumn.config.ts. seed: { autumn: { plans: AUTUMN_PLAN_SEED } }, }); const workosUrl = options.workosPublicUrl ?? workos.url; const env = { // Real client, emulated service. The app derives the browser-facing // authorize URL from WORKOS_API_URL, so it must be the PUBLIC origin. WORKOS_API_URL: workosUrl, AUTUMN_API_URL: autumn.url, WORKOS_API_KEY: "sk_test_emulate", WORKOS_CLIENT_ID: options.workosClientId, WORKOS_COOKIE_PASSWORD: options.cookiePassword, AUTUMN_SECRET_KEY: "am_test_emulate", ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", DATABASE_URL: `postgresql://postgres:postgres@127.0.0.1:${options.dbPort}/postgres`, EXECUTOR_DIRECT_DATABASE_URL: "true", CLOUDFLARE_INCLUDE_PROCESS_ENV: "true", VITE_PUBLIC_SITE_URL: options.publicUrl, // The AuthKit domain (MCP OAuth metadata + JWKS) is the emulator too. MCP_AUTHKIT_DOMAIN: workosUrl, MCP_RESOURCE_ORIGIN: options.publicUrl, MCP_SESSION_TIMEOUT_MS: process.env.MCP_SESSION_TIMEOUT_MS, MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: process.env.MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS, ALLOW_LOCAL_NETWORK: "true", // A first-party GitHub app for the first-party-oauth scenario: proves the // env → HostConfig → executor plumbing end to end. The scenario asserts the // authorize REDIRECT only (client id + callback), never visits github.com. FIRST_PARTY_GITHUB_CLIENT_ID: "e2e-first-party-github", FIRST_PARTY_GITHUB_CLIENT_SECRET: "e2e-first-party-github-secret", // Optional endpoint overrides pass through so a dev instance (`cli up // cloud`) can point the first-party app at an emulated GitHub and run the // complete authorize → token dance instead of stopping at github.com. FIRST_PARTY_GITHUB_AUTHORIZE_URL: process.env.FIRST_PARTY_GITHUB_AUTHORIZE_URL, FIRST_PARTY_GITHUB_TOKEN_URL: process.env.FIRST_PARTY_GITHUB_TOKEN_URL, // Fake Google registration for redirect-only first-party coverage. The // scenario never visits Google or exchanges a code; it proves cloud env, // scope policy, and authorization URL construction end to end. FIRST_PARTY_GOOGLE_CLIENT_ID: "e2e-first-party-google", FIRST_PARTY_GOOGLE_CLIENT_SECRET: "e2e-first-party-google-secret", // Shrink the per-org hourly execution cap (prod default 1000) to a number // the rate-limit-backstop scenario can actually exhaust with real // executions — but see execution-limits.ts: it must stay above every other // scenario's per-org execute count. Reaches the worker via // CLOUDFLARE_INCLUDE_PROCESS_ENV, same as ALLOW_LOCAL_NETWORK. EXECUTION_RATE_LIMIT_PER_HOUR: String(E2E_EXECUTION_RATE_LIMIT), // Set to a value that is NOT the compiled-in default, so the span the // worker exports proves this override was actually read rather than // silently ignored (execution-limits.ts explains why it is longer, not // shorter, than the default). EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS: String(E2E_EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS), // Throwaway PGlite on its own port + dir so it never fights `bun dev`. DEV_DB_PORT: String(options.dbPort), DEV_DB_PATH: dbPath, // Vite rejects unknown Host headers; allow the public hostname when a // proxy fronts the app. __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: new URL(options.publicUrl).hostname, ...options.extraEnv, }; const procs = bootProcesses( [ { cmd: "bun", args: ["run", "scripts/dev-db.ts"], cwd: cloudDir, env, logFile: options.logFile, }, { cmd: "bunx", args: [ "vite", "dev", "--port", String(options.cloudPort), "--strictPort", "--host", options.host ?? "127.0.0.1", ], cwd: cloudDir, // Mitigation, not a fix (the real per-request OOM growth is what the // spec-compile LRU addresses): CI runners have 7GB, well above the // default ~2GB V8 old-space ceiling, so give the dev server room // before it hits the limit and starts 500ing. Scoped to this child // only; dev-db does not need it. env: { ...env, NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --max-old-space-size=3072`.trim(), }, logFile: options.logFile, }, ], { label: "cloud" }, ); const teardown = async () => { await procs.teardown(); await workos.close(); await autumn.close(); }; try { const local = `http://127.0.0.1:${options.cloudPort}`; await waitForBoot(procs, (signal) => waitForHttp(local, { signal })); // The API plane is ready when login actually redirects to AuthKit. await waitForBoot(procs, (signal) => waitForHttp(`${local}/api/auth/login`, { expectRedirect: true, signal }), ); } catch (error) { await teardown(); throw error; } return { teardown, pids: procs.pids, workosUrl, autumnUrl: autumn.url }; };