// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The app as a production build with AUTH_PROVIDER=convex-better-auth, for gate J-auth (joint // contract ยง14): a disposable Better Auth deployment on the platform's pinned self-hosted Convex // image, a Postgres with the three roles, the fake platform of @conusai-kit/testing answering the // real platform API (the app runs in platform mode, as in production: Support mints widget // sessions and Guides bootstraps through it), the migrate job (which deploys the Better Auth // functions), the worker and `next start`. Containers publish on 127.0.0.1 only (rootless Docker), // and every container and volume is removed when the run ends. // // Run it with `pnpm run build` first, then // pnpm exec playwright test -c e2e/better-auth/playwright.config.ts import { type ChildProcess, execFileSync, spawn } from "node:child_process" import { randomBytes } from "node:crypto" import { mkdirSync, openSync, readFileSync } from "node:fs" import { join } from "node:path" import { startFakePlatform } from "@conusai-kit/testing/fake-platform" export const APP_ROOT = new URL("../..", import.meta.url).pathname const LOGS = join(APP_ROOT, "e2e", ".logs") /** The platform's pinned self-hosted Convex backend (images/third-party.toml `convex-backend`). */ export const CONVEX_BACKEND_IMAGE = process.env.CONVEX_BACKEND_IMAGE ?? "build.conusai.com/conusai/convex-backend@sha256:1738f1673f8d63161043a7859710d2301b1e9d6271e06afbb7af31594ea3a58f" const WEB_PORT = Number(process.env.JAUTH_WEB_PORT ?? 3150) /** What the specs read: the app, the fake platform and the Better Auth deployment's server side. */ export interface JauthStack { readonly baseUrl: string readonly platformUrl: string readonly convexUrl: string readonly convexSiteUrl: string readonly adminKey: string readonly backend: string /** Server-side values a browser must never see: the network inspection looks for each. */ readonly secrets: readonly string[] } const sh = (command: string, args: string[], env?: NodeJS.ProcessEnv): string => execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...(env ? { env } : {}) }).trim() async function until(label: string, check: () => Promise | boolean, seconds = 120): Promise { const deadline = Date.now() + seconds * 1000 for (;;) { try { if (await check()) return } catch { // not yet } if (Date.now() > deadline) throw new Error(`j-auth: ${label} did not come up in ${seconds}s (logs in e2e/.logs)`) await new Promise((resolve) => setTimeout(resolve, 500)) } } function spawnLogged(name: string, command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv): ChildProcess { mkdirSync(LOGS, { recursive: true }) const out = openSync(join(LOGS, `jauth-${name}.log`), "w") return spawn(command, args, { cwd, env, stdio: ["ignore", out, out], detached: true }) } function stopGroup(child: ChildProcess): void { if (child.pid === undefined) return try { process.kill(-child.pid, "SIGTERM") } catch { // already gone } } /** The host port Docker published for a container port, on 127.0.0.1. */ const portOf = (container: string, port: number): number => Number( sh("docker", ["port", container, `${port}/tcp`]) .split("\n")[0] ?.split(":") .at(-1), ) const removeContainer = (name: string): void => { try { // -v: the anonymous data volume goes with the container. sh("docker", ["rm", "-f", "-v", name]) } catch { // already gone } } export async function startJauthStack(): Promise<{ stack: JauthStack; stop(): Promise }> { const run = randomBytes(4).toString("hex") const backend = `jauth-better-auth-${run}` const postgres = `jauth-postgres-${run}` const processes: ChildProcess[] = [] const platform = await startFakePlatform() const stop = async (): Promise => { for (const child of processes) stopGroup(child) await platform.close() removeContainer(backend) removeContainer(postgres) } try { sh("docker", [ "run", "-d", "--name", backend, "-l", "conusai.test=j-auth", "-e", `INSTANCE_NAME=jauth_${run}`, "-e", `INSTANCE_SECRET=${randomBytes(32).toString("hex")}`, // The backend reaches its own site URL (the issuer of Better Auth's Convex tokens) inside the container. "-e", "CONVEX_CLOUD_ORIGIN=http://127.0.0.1:3210", "-e", "CONVEX_SITE_ORIGIN=http://127.0.0.1:3211", "-p", "127.0.0.1::3210", "-p", "127.0.0.1::3211", CONVEX_BACKEND_IMAGE, ]) sh("docker", [ "run", "-d", "--name", postgres, "-l", "conusai.test=j-auth", "-p", "127.0.0.1::5432", "-e", "POSTGRES_PASSWORD=postgres", "-e", "POSTGRES_DB=appdev", "-v", `${join(APP_ROOT, "dev", "postgres-roles.sql")}:/docker-entrypoint-initdb.d/10-roles.sql:ro`, "pgvector/pgvector:pg18", ]) const convexUrl = `http://127.0.0.1:${portOf(backend, 3210)}` const convexSiteUrl = `http://127.0.0.1:${portOf(backend, 3211)}` const pgPort = portOf(postgres, 5432) const pg = (role: string) => `postgresql://appdev_${role}:appdev_${role}@127.0.0.1:${pgPort}/appdev` await until("the Better Auth backend", async () => (await fetch(`${convexUrl}/version`)).ok) const adminKey = sh("docker", ["exec", backend, "./generate_admin_key.sh"]).split("\n").at(-1) ?? "" if (!adminKey) throw new Error("j-auth: the backend generated no admin key") await until("postgres", () => { sh("docker", ["exec", postgres, "psql", "-U", "appdev_app", "-h", "127.0.0.1", "-d", "appdev", "-c", "select 1"]) return true }) const baseUrl = `http://localhost:${WEB_PORT}` const betterAuthSecret = randomBytes(32).toString("base64url") const platformToken = `jauth-platform-token-${randomBytes(8).toString("hex")}` const sessionSecret = randomBytes(32).toString("base64url") // A production run against the platform API (the fake platform answers it): no fake-platform // switch, no dev sign-in, no insecure cookies, no OIDC variable -- what a deployment has. const common: NodeJS.ProcessEnv = { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR, NODE_ENV: "production", CONUSAI_API_URL: `${platform.url}/api`, CONUSAI_API_TOKEN: platformToken, CONUSAI_PROJECT_ID: "42", CONUSAI_ORG_ID: "jauth", CONUSAI_ORG_SLUG: "jauth", CONUSAI_PUBLIC_URL: baseUrl, CONUSAI_RELEASE: "j-auth", CONUSAI_SUPPORT_CHANNEL: "spk_jauth", // kit:support POSTGRES_URL: pg("app"), POSTGRES_READER_URL: pg("reader"), KNOWLEDGE_VECTOR_STORE: "pgvector", AUTH_SESSION_SECRET: sessionSecret, AUTH_PROVIDER: "convex-better-auth", AUTH_IDENTITY_NAMESPACE: "better-auth:jauth", BETTER_AUTH_CONVEX_URL: convexUrl, BETTER_AUTH_CONVEX_SITE_URL: convexSiteUrl, BETTER_AUTH_CONVEX_ADMIN_KEY: adminKey, BETTER_AUTH_RELAY_INTERVAL_MS: "250", } // The migrate job: the schema, then the Better Auth functions (it alone holds BETTER_AUTH_SECRET). execFileSync(process.execPath, ["apps/worker/dist/migrate.js"], { cwd: APP_ROOT, env: { ...common, POSTGRES_MIGRATE_URL: pg("owner"), BETTER_AUTH_SECRET: betterAuthSecret }, stdio: "inherit", }) // The deployed functions typecheck against the contexts Convex generated for them (deploy.mjs // pushes without a typecheck: the runtime image carries no TypeScript). execFileSync( process.execPath, [join(APP_ROOT, "node_modules/typescript/bin/tsc"), "-p", "better-auth/functions/tsconfig.json", "--noEmit"], { cwd: APP_ROOT, stdio: "inherit" }, ) processes.push( spawnLogged("worker", process.execPath, ["apps/worker/dist/main.js"], APP_ROOT, { ...common, WORKER_HEARTBEAT_FILE: join(LOGS, "jauth-worker-heartbeat"), }), ) processes.push( spawnLogged( "web", process.execPath, ["node_modules/next/dist/bin/next", "start", "-p", String(WEB_PORT), "-H", "localhost"], join(APP_ROOT, "apps", "web"), common, ), ) await until("the web shell", async () => (await fetch(`${baseUrl}/api/ready`)).ok, 120) await until( "the worker", () => readFileSync(join(LOGS, "jauth-worker.log"), "utf8").includes("worker: serving"), 60, ) return { stack: { baseUrl, platformUrl: platform.url, convexUrl, convexSiteUrl, adminKey, backend, secrets: [adminKey, betterAuthSecret, platformToken, sessionSecret], }, stop, } } catch (error) { await stop() throw error } }