181 lines · 7.7 KB · TypeScript
RawDownload
// 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 } 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<string, string>;} export interface CloudBooted {  readonly teardown: () => Promise<void>;  readonly pids: ReadonlyArray<number>;  readonly workosUrl: string;  readonly autumnUrl: string;} export const bootCloud = async (options: CloudBootOptions): Promise<CloudBooted> => {  // 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 });   // 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_REQUEST_STATE_KEY: "e2e-mcp-request-state-key-0123456789abcdef",    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),    // 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 };};