// A Target is one deployed shape of the product (cloud / selfhost / …) seen // purely from the outside: base URLs, how to mint a fresh isolated identity, // and which capabilities it supports. Scenarios are written once against this // interface; vitest projects pick which target a run executes on via // E2E_TARGET. Boot/teardown of the instance is NOT here — each app owns its // own dev-server boot (see setup/*.globalsetup.ts which call into the app). import type { Effect } from "effect"; export type Capability = | "api" // typed HttpApiClient over the wire | "browser" // web UI reachable + identity injectable into a browser context | "mcp-oauth" // MCP endpoint with a headless OAuth consent path | "cli" // a CLI/TUI entry point exists for this target | "billing" // billing limits are enforced (cloud-only) | "opencode" // the real OpenCode binary is installed on this host | "ttl-control"; // the authorization server's access-token TTL is test-adjustable export interface Identity { /** Shown in transcripts ("user_ab12cd") */ readonly label: string; /** Headers that authenticate API requests (e.g. a session cookie). */ readonly headers?: Record; /** Cookies to inject into a browser context for a logged-in page. */ readonly cookies?: ReadonlyArray<{ readonly name: string; readonly value: string }>; /** Credentials for surfaces that sign in themselves (Better Auth, OAuth consent). */ readonly credentials?: { readonly email: string; readonly password: string }; } export interface Target { readonly name: string; readonly baseUrl: string; readonly mcpUrl: string; readonly capabilities: ReadonlySet; /** * Mint a fresh identity — THE isolation model: no resets, every scenario * is its own user (and org where applicable) on the shared instance. * `org: false` yields an identity with no active organization (for flows * that create one, like onboarding / billing limits). */ readonly newIdentity: (options?: { readonly org?: boolean }) => Effect.Effect; /** Headless OAuth consent for the MCP surface, when "mcp-oauth" is supported. */ readonly mcpConsent?: ( identity: Identity, ) => (request: { authorizationUrl: string }) => Promise<{ code: string }>; /** * Compress (or restore, with null) the authorization server's access-token * lifetime, when "ttl-control" is supported — what lets token-expiry * scenarios cross a REAL expiry in seconds instead of an hour. */ readonly setAccessTokenTtl?: (seconds: number | null) => Effect.Effect; }