#!/usr/bin/env node import { execFileSync } from "node:child_process"; import { randomBytes } from "node:crypto"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; const API_BASE_URL = trimTrailingSlash( process.env.API_BASE_URL ?? "https://cfw-auth.bowong.cc/api/auth", ); const TRUSTED_ORIGIN = process.env.API_TEST_ORIGIN ?? "http://localhost:8787"; const RUN_ID = process.env.RUN_ID ?? `api-test-${new Date().toISOString().slice(0, 10)}`; const ENV_FILE = process.env.API_TEST_ENV_FILE ?? ".api-test.env"; const D1_DATABASE = process.env.API_TEST_D1_DATABASE ?? "cfw-auth"; const USE_REMOTE_D1 = process.env.API_TEST_USE_REMOTE_D1 !== "false"; const existingEnv = readEnvFile(ENV_FILE); const password = existingEnv.API_TEST_PASSWORD ?? makePassword(); const adminPassword = existingEnv.API_TEST_ADMIN_PASSWORD ?? makePassword(); const userEmail = existingEnv.API_TEST_USER_EMAIL ?? `${slug(RUN_ID)}-user@example.invalid`; const adminEmail = existingEnv.API_TEST_ADMIN_EMAIL ?? `${slug(RUN_ID)}-admin@example.invalid`; const organizationSlug = existingEnv.API_TEST_ORGANIZATION_SLUG ?? `${slug(RUN_ID)}-org`.slice(0, 48); const state = { API_BASE_URL, API_TEST_ORIGIN: TRUSTED_ORIGIN, RUN_ID, API_TEST_USER_EMAIL: userEmail, API_TEST_PASSWORD: password, API_TEST_ADMIN_EMAIL: adminEmail, API_TEST_ADMIN_PASSWORD: adminPassword, API_TEST_ORGANIZATION_SLUG: organizationSlug, }; await ensureSignedUp({ email: userEmail, password, name: `API Test User ${RUN_ID}`, }); await ensureSignedUp({ email: adminEmail, password: adminPassword, name: `API Test Admin ${RUN_ID}`, }); const databaseUser = getUserByEmail(userEmail); const databaseAdmin = getUserByEmail(adminEmail); if (!databaseUser || !databaseAdmin) { throw new Error("Created test users were not found in D1."); } markVerifiedAndAdmin({ userId: databaseUser.id, adminId: databaseAdmin.id, }); let userSession = await signIn({ email: userEmail, password }); state.API_TEST_SESSION_COOKIE = userSession.cookie; state.API_TOKEN = userSession.token ?? ""; state.API_TEST_USER_ID = userSession.user.id; const adminSession = await signIn({ email: adminEmail, password: adminPassword }); state.API_TEST_ADMIN_SESSION_COOKIE = adminSession.cookie; state.API_TEST_ADMIN_TOKEN = adminSession.token ?? ""; state.API_TEST_ADMIN_ID = adminSession.user.id; const organization = await ensureOrganization({ cookie: userSession.cookie, name: `API Test Org ${RUN_ID}`, slug: organizationSlug, }); if (organization?.id) { userSession = { ...userSession, cookie: await setActiveOrganization({ cookie: userSession.cookie, organizationId: organization.id, }), }; state.API_TEST_SESSION_COOKIE = userSession.cookie; state.API_TEST_ORGANIZATION_ID = organization.id; } const apiKey = await createApiKey({ cookie: userSession.cookie, runId: RUN_ID }); if (apiKey?.id) { state.API_TEST_API_KEY_ID = apiKey.id; } if (apiKey?.key) { state.API_TEST_API_KEY = apiKey.key; } writeEnvFile(ENV_FILE, state); console.log( JSON.stringify( { envFile: ENV_FILE, apiBaseUrl: API_BASE_URL, runId: RUN_ID, user: { id: state.API_TEST_USER_ID, email: userEmail }, admin: { id: state.API_TEST_ADMIN_ID, email: adminEmail }, organization: state.API_TEST_ORGANIZATION_ID ? { id: state.API_TEST_ORGANIZATION_ID, slug: organizationSlug } : null, apiKey: state.API_TEST_API_KEY_ID ? { id: state.API_TEST_API_KEY_ID } : null, }, null, 2, ), ); function trimTrailingSlash(value) { return value.replace(/\/+$/, ""); } function slug(value) { return value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 40); } function makePassword() { return `ApiTest-${randomBytes(12).toString("hex")}-Aa1!`; } function readEnvFile(path) { if (!existsSync(path)) return {}; const out = {}; for (const line of readFileSync(path, "utf8").split(/\r?\n/)) { if (!line || line.trimStart().startsWith("#")) continue; const index = line.indexOf("="); if (index === -1) continue; const key = line.slice(0, index).trim(); const raw = line.slice(index + 1).trim(); out[key] = raw.replace(/^"|"$/g, "").replace(/\\"/g, '"'); } return out; } function writeEnvFile(path, values) { const lines = [ "# Generated by scripts/bootstrap-api-test-fixtures.mjs", "# Contains test credentials. Do not commit.", ]; for (const [key, value] of Object.entries(values)) { lines.push(`${key}=${quoteEnv(String(value ?? ""))}`); } writeFileSync(path, `${lines.join("\n")}\n`, "utf8"); } function quoteEnv(value) { return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; } async function ensureSignedUp({ email, password, name }) { const response = await request("/sign-up/email", { method: "POST", body: { email, password, name, rememberMe: true, }, }); if (response.status === 200) return response.body; if (response.status === 422 || response.status === 400) { const user = getUserByEmail(email); if (user) return { user }; } throw new Error( `Failed to create or load ${email}: ${response.status} ${JSON.stringify(response.body)}`, ); } async function signIn({ email, password }) { const response = await request("/sign-in/email", { method: "POST", body: { email, password, rememberMe: true, }, }); if (response.status !== 200) { throw new Error(`Failed to sign in ${email}: ${response.status} ${JSON.stringify(response.body)}`); } const cookie = sessionCookieFromSetCookie(response.setCookie); if (!cookie) throw new Error(`Sign-in for ${email} did not return a session cookie.`); const session = await request("/get-session", { cookie }); if (session.status !== 200 || !session.body?.user?.id) { throw new Error(`Failed to read session for ${email}: ${session.status}`); } return { cookie, token: response.body?.token, user: session.body.user }; } async function ensureOrganization({ cookie, name, slug }) { const response = await request("/organization/create", { method: "POST", cookie, body: { name, slug, keepCurrentActiveOrganization: true, metadata: { runId: RUN_ID }, }, }); if (response.status === 200) return response.body; if (response.status === 400 || response.status === 422) { const existing = getOrganizationBySlug(slug); if (existing) return existing; } console.warn(`Organization fixture not created: ${response.status} ${JSON.stringify(response.body)}`); return null; } async function setActiveOrganization({ cookie, organizationId }) { const response = await request("/organization/set-active", { method: "POST", cookie, body: { organizationId }, }); if (response.status !== 200) { throw new Error( `Failed to set active organization ${organizationId}: ${response.status} ${JSON.stringify(response.body)}`, ); } return sessionCookieFromSetCookie(response.setCookie) || cookie; } async function createApiKey({ cookie, runId }) { if (existingEnv.API_TEST_API_KEY && existingEnv.API_TEST_API_KEY_ID) { return { id: existingEnv.API_TEST_API_KEY_ID, key: existingEnv.API_TEST_API_KEY }; } const response = await request("/api-key/create", { method: "POST", cookie, body: { name: `api-test-${runId}`, expiresIn: null, remaining: null, metadata: { runId }, }, }); if (response.status === 200) return response.body; console.warn(`API key fixture not created: ${response.status} ${JSON.stringify(response.body)}`); return null; } async function request(path, { method = "GET", body, cookie } = {}) { const headers = { accept: "application/json", origin: TRUSTED_ORIGIN, }; if (body !== undefined) headers["content-type"] = "application/json"; if (cookie) headers.cookie = cookie; const response = await fetch(`${API_BASE_URL}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), redirect: "manual", }); const text = await response.text(); return { status: response.status, body: text ? JSON.parse(text) : null, setCookie: response.headers.getSetCookie?.() ?? splitSetCookie(response.headers.get("set-cookie")), }; } function splitSetCookie(value) { if (!value) return []; return value.split(/,(?=\s*[^;,]+=)/g); } function sessionCookieFromSetCookie(setCookieHeaders) { const cookies = new Map(); for (const entry of setCookieHeaders .map((entry) => entry.split(";")[0]) .filter((entry) => /better-auth\.session|session_token|session/.test(entry))) { const index = entry.indexOf("="); if (index !== -1) cookies.set(entry.slice(0, index), entry); } return [...cookies.values()].join("; "); } function markVerifiedAndAdmin({ userId, adminId }) { const userIdSql = sqlString(userId); const adminIdSql = sqlString(adminId); const sql = [ `update "user" set "emailVerified" = 1 where "id" in (${userIdSql}, ${adminIdSql});`, `update "user" set "role" = 'admin' where "id" = ${adminIdSql};`, ].join(" "); runD1(sql); } function getUserByEmail(email) { const rows = runD1( `select "id", "name", "email", "emailVerified", "role" from "user" where "email" = ${sqlString( email.toLowerCase(), )} limit 1;`, ); return rows[0] ?? null; } function getOrganizationBySlug(organizationSlugValue) { const rows = runD1( `select "id", "name", "slug", "createdAt" from "organization" where "slug" = ${sqlString( organizationSlugValue, )} limit 1;`, ); return rows[0] ?? null; } function runD1(command) { const args = ["wrangler", "d1", "execute", D1_DATABASE, "--command", command, "--json"]; if (USE_REMOTE_D1) args.push("--remote"); const raw = execFileSync("pnpm", ["exec", ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); const parsed = JSON.parse(raw); const first = Array.isArray(parsed) ? parsed[0] : parsed; if (!first?.success) { throw new Error(`D1 command failed: ${JSON.stringify(parsed)}`); } return first.results ?? []; } function sqlString(value) { return `'${String(value).replace(/'/g, "''")}'`; }