feat: create the initial platform owner from an operator command
CI / verify (pull_request) Successful in 41s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 1m18s

A fresh production installation deadlocked. ADR 0034 gave Ekklesia a login
page and ADR 0019 gave it a single-use platform-owner bootstrap, but that
bootstrap grants ownership to a user id that already exists and has no way
to bring one into being. Public signup is disabled by default and
production cannot enable it at all, so a newly deployed instance had a
login page, no account, and no supported way to create one — only hand-
written SQL or briefly opening public registration.

    pnpm identity:create-initial-owner -- --email [email protected]

creates the identity and grants it ownership, in the same shape as the
other operator commands: trusted local tooling, reachable only by someone
who already holds the database credentials, serving no HTTP route. No
/signup, /register, or /setup route is added, no bootstrap endpoint, and
no setup token; public signup stays disabled in every environment and a
test asserts a freshly bootstrapped deployment still refuses
/api/auth/sign-up/email.

The identity is created through Better Auth's own sign-up path, so
hashing, salting, normalization and the 8-128 bound are the identity
system's rather than a second implementation that could drift. Because the
server's instance refuses account creation, the command builds its own
in-process instance; nothing in apps/api imports it and the running
server's posture is untouched.

The sign-up result is verified rather than trusted. Better Auth returns a
fabricated success for an already-registered address — a user object whose
id was never persisted, database unchanged. That is correct enumeration
defence on a public endpoint and a trap here, so the identity is always
read back from the database and that read, not the response, is what the
grant uses.

An existing usable identity is adopted: ownership is granted and its
password left alone, reported as an adoption rather than a creation. This
is the recovery path for the one partial failure possible, since sign-up
commits on Better Auth's connection and the grant is a separate
transaction. A credential-less row is refused instead — making it an owner
would consume the single use on an identity nobody can authenticate as. A
password is never reset and an account is never taken over.

Whole invocations are serialized by an advisory lock on their own key, so
a race cannot leave a stray signable-in account behind; the grant's own
locked single-use transaction is unchanged. There is no --force or reset.

The password is prompted for invisibly or read from stdin, never from
argv (--password is refused with an explanation) and never from an
environment variable, so nothing about it becomes permanent deployment
configuration. The command needs DATABASE_URL and nothing else — verified
that sign-up needs no secret, base URL, or trusted origins, and that the
resulting account signs in through a separately configured server — so the
first owner can be created before TLS or DNS exist. It never migrates.

Proven end to end rather than at the database: a fresh installation is
bootstrapped, signed in through the real browser login transport with
signup disabled, its principal read, and used to open an owner-protected
resource that stays closed to anonymous callers.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-08-23 22:32:53 -05:00
co-authored by Claude Opus 5
parent c131543abf
commit c8fa13dd6a
16 changed files with 1767 additions and 12 deletions
+687
View File
@@ -0,0 +1,687 @@
import { execFile } from "node:child_process";
import { randomBytes } from "node:crypto";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import {
createAuthorizationDrizzleDatabase,
createInitialPlatformOwner,
InitialPlatformOwnerError,
} from "@ekklesia/core/authorization";
import {
createBootstrapIdentityCreator,
createIdentityDrizzleDatabase,
} from "@ekklesia/core/identity";
import {
applyModuleMigrations,
createPostgresMigrationDatabase,
discoverAllMigrations,
} from "@ekklesia/core/migrations";
import { Client, type QueryResultRow } from "pg";
import { afterAll, describe, expect, it } from "vitest";
import { createApiRuntimeConfig } from "../src/config.js";
import { type RunningApi, startApi } from "../src/server.js";
/**
* The initial platform-owner bootstrap, against a real PostgreSQL and the real HTTP transport
* (ADR 0035).
*
* The point of this suite is that it does **not** stop at asserting rows. A fresh installation is
* bootstrapped and then the resulting account is signed in through the exact browser-facing login
* transport Phase 7B Pass 1 established, its principal is read back, and it is used to reach a
* platform-owner-protected resource. Anything less would prove the command wrote a user, not that
* an operator can actually get into their dashboard — which is the only thing this pass is for.
*
* Every scenario runs with `publicSignupEnabled: false`, the production posture, so nothing here
* can accidentally depend on the signup route the real deployment does not have.
*/
const integrationDatabaseUrlEnvironment = "EKKLESIA_MIGRATION_TEST_DATABASE_URL";
const repositoryRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
const coreMigrationsDirectory = path.join(repositoryRoot, "core", "drizzle");
const bootstrapCliPath = path.join(
repositoryRoot,
"core",
"dist",
"cli",
"create-initial-owner.js",
);
const trackedDatabases = new Set<string>();
let databaseCounter = 0;
const identityBaseUrl = "https://identity.example.test";
const dashboardOrigin = "https://dashboard.example.test";
const ownerPassword = "correct horse battery staple";
class IntegrationHarnessError extends Error {
constructor(message: string) {
super(message);
this.name = "IntegrationHarnessError";
this.stack = `${this.name}: ${this.message}`;
}
}
function baseDatabaseUrl(): string {
const url = process.env[integrationDatabaseUrlEnvironment];
if (typeof url !== "string" || url.trim() === "") {
throw new IntegrationHarnessError("The integration database URL is not configured.");
}
return url;
}
function databaseUrl(name: string): string {
const url = new URL(baseDatabaseUrl());
url.pathname = `/${name}`;
return url.toString();
}
function makeDatabaseName(label: string): string {
databaseCounter += 1;
return `ekk_owner_${label}_${databaseCounter}_${randomBytes(4).toString("hex")}`.toLowerCase();
}
async function openAdministrativeClient(): Promise<Client> {
const client = new Client({ connectionString: baseDatabaseUrl() });
await client.connect();
return client;
}
async function safeQuery<T extends QueryResultRow>(
client: Client,
text: string,
values: readonly unknown[] = [],
): Promise<T[]> {
try {
const result = await client.query<T>(text, [...values]);
return result.rows;
} catch {
// Never rethrows the driver error: it can carry SQL, a connection string, and a host.
throw new IntegrationHarnessError("An integration database statement failed.");
}
}
async function createDisposableDatabase(label: string): Promise<string> {
const name = makeDatabaseName(label);
const admin = await openAdministrativeClient();
try {
await safeQuery(admin, `create database "${name}"`);
} finally {
await admin.end().catch(() => undefined);
}
trackedDatabases.add(name);
return databaseUrl(name);
}
async function applyCoreMigrations(url: string): Promise<void> {
const emptyModules = await mkdtemp(path.join(tmpdir(), "ekklesia-owner-no-modules-"));
try {
const aggregate = await discoverAllMigrations({
coreMigrationsDirectory,
moduleDiscovery: {
moduleSdkVersion: "0.1.0",
modulesDirectory: emptyModules,
platformVersion: "0.1.0",
},
});
await applyModuleMigrations({ aggregate, database: createPostgresMigrationDatabase(url) });
} finally {
await rm(emptyModules, { force: true, recursive: true });
}
}
/** A fresh, migrated installation with no identity and no owner — exactly what an operator has
* after `docker compose up` and `pnpm migrations:apply`. */
async function freshInstallation(label: string): Promise<string> {
const url = await createDisposableDatabase(label);
await applyCoreMigrations(url);
return url;
}
interface BootstrapHandles {
readonly run: (
email: string,
password?: string,
name?: string,
) => ReturnType<typeof createInitialPlatformOwner>;
readonly close: () => Promise<void>;
}
/** Drives the same operation the CLI drives, with the same production composition. */
function bootstrapAgainst(url: string): BootstrapHandles {
const authorization = createAuthorizationDrizzleDatabase(url);
const identity = createIdentityDrizzleDatabase(url, {});
return {
close: async () => {
await Promise.all([
authorization.close().catch(() => undefined),
identity.close().catch(() => undefined),
]);
},
run: (email, password = ownerPassword, name = "Platform Owner") =>
createInitialPlatformOwner(
{
authorizationDb: authorization.db,
createIdentity: createBootstrapIdentityCreator(identity.db),
identityDb: identity.db,
},
{ email, name, password },
),
};
}
/** The production posture: public signup disabled, exactly as a real deployment must run. */
async function startApiWithoutSignup(url: string): Promise<RunningApi> {
const config = createApiRuntimeConfig(
{
databaseUrl: url,
host: "127.0.0.1",
identity: {
baseUrl: identityBaseUrl,
publicSignupEnabled: false,
secret: "integration-test-secret-0123456789abcdefgh",
trustedOrigins: [identityBaseUrl, dashboardOrigin],
},
port: 0,
},
{ environment: "development" },
);
return startApi(config, { rateLimitOverride: { max: 1000, window: 1 } });
}
function apiOrigin(running: RunningApi): string {
const address = running.server.address();
if (!address || typeof address !== "object") {
throw new IntegrationHarnessError("The API integration server has no bound address.");
}
return `http://127.0.0.1:${address.port}`;
}
function firstCookiePair(setCookieHeader: string): string {
return setCookieHeader.split(",")[0]?.split(";")[0]?.trim() ?? "";
}
async function signIn(
origin: string,
email: string,
password: string,
): Promise<{ cookiePair?: string; response: Response }> {
const response = await fetch(`${origin}/api/auth/sign-in/email`, {
body: JSON.stringify({ email, password }),
headers: { "content-type": "application/json", origin: dashboardOrigin },
method: "POST",
});
const setCookie = response.headers.get("set-cookie");
return setCookie === null ? { response } : { cookiePair: firstCookiePair(setCookie), response };
}
const execFileAsync = promisify(execFile);
interface CliOutcome {
readonly code: number;
readonly stdout: string;
readonly stderr: string;
}
/** Runs the real compiled CLI as a real child process, with the password on stdin and never in
* `argv`. */
async function runBootstrapCli(
databaseUrl: string | undefined,
args: readonly string[],
password: string | undefined,
): Promise<CliOutcome> {
const environment: Record<string, string> = {
...(process.env as Record<string, string>),
NODE_ENV: "development",
};
if (databaseUrl === undefined) {
delete environment.DATABASE_URL;
} else {
environment.DATABASE_URL = databaseUrl;
}
const child = execFileAsync(process.execPath, [bootstrapCliPath, ...args], {
env: environment,
});
child.child.stdin?.end(password ?? "");
try {
const { stdout, stderr } = await child;
return { code: 0, stderr, stdout };
} catch (error) {
const failure = error as { code?: number; stdout?: string; stderr?: string };
return {
code: typeof failure.code === "number" ? failure.code : 1,
stderr: failure.stderr ?? "",
stdout: failure.stdout ?? "",
};
}
}
async function countRows(url: string, table: string): Promise<number> {
const client = new Client({ connectionString: url });
await client.connect();
try {
const rows = await safeQuery<{ count: string }>(
client,
`select count(*)::text from "${table}"`,
);
return Number(rows[0]?.count ?? "0");
} finally {
await client.end().catch(() => undefined);
}
}
afterAll(async () => {
const admin = await openAdministrativeClient().catch(() => undefined);
if (!admin) {
return;
}
try {
for (const name of trackedDatabases) {
await safeQuery(admin, `drop database if exists "${name}" with (force)`).catch(
() => undefined,
);
}
} finally {
await admin.end().catch(() => undefined);
}
});
describe.sequential("initial platform-owner bootstrap", () => {
it("takes a fresh installation all the way to an authenticated owner in the browser", async () => {
const url = await freshInstallation("browserproof");
const email = `owner-${randomBytes(4).toString("hex")}@example.test`;
const bootstrap = bootstrapAgainst(url);
let created: Awaited<ReturnType<typeof createInitialPlatformOwner>>;
try {
created = await bootstrap.run(email);
} finally {
await bootstrap.close();
}
expect(created.identity).toBe("created");
expect(created.email).toBe(email.toLowerCase());
const api = await startApiWithoutSignup(url);
try {
const origin = apiOrigin(api);
// 1. The exact browser-facing login transport, on a deployment where signup does not exist.
const session = await signIn(origin, email, ownerPassword);
expect(session.response.status).toBe(200);
expect(session.cookiePair).toBeDefined();
const cookie = session.cookiePair as string;
// 2. The principal the browser bootstrap reads.
const principalResponse = await fetch(`${origin}/api/v1/principal`, { headers: { cookie } });
expect(principalResponse.status).toBe(200);
const principal = (await principalResponse.json()) as {
kind: string;
userId?: string;
};
expect(principal.kind).toBe("authenticated");
expect(principal.userId).toBe(created.userId);
// 3. A platform-owner-protected resource actually opens for them. This is the assertion that
// separates "a row was written" from "the operator can run their platform".
const grants = await fetch(`${origin}/api/v1/grants?userId=${created.userId}`, {
headers: { cookie },
});
expect(grants.status).toBe(200);
// 4. And the same resource is still closed to everyone else.
const anonymous = await fetch(`${origin}/api/v1/grants?userId=${created.userId}`);
expect(anonymous.status).toBe(403);
} finally {
await api.shutdown();
}
});
it("leaves public signup disabled — the owner exists without a signup route existing", async () => {
const url = await freshInstallation("nosignup");
const email = `owner-${randomBytes(4).toString("hex")}@example.test`;
const bootstrap = bootstrapAgainst(url);
try {
await bootstrap.run(email);
} finally {
await bootstrap.close();
}
const api = await startApiWithoutSignup(url);
try {
const origin = apiOrigin(api);
const attempted = await fetch(`${origin}/api/auth/sign-up/email`, {
body: JSON.stringify({
email: `intruder-${randomBytes(4).toString("hex")}@example.test`,
name: "Intruder",
password: ownerPassword,
}),
headers: { "content-type": "application/json", origin: dashboardOrigin },
method: "POST",
});
// The bootstrap command created an account; it did not open a door for anyone else to.
expect(attempted.status).toBe(400);
expect(await attempted.text()).toContain("EMAIL_PASSWORD_SIGN_UP_DISABLED");
expect(await countRows(url, "ekklesia_core_user")).toBe(1);
// And the owner can still sign in, so disabling signup did not disable the account.
expect((await signIn(origin, email, ownerPassword)).response.status).toBe(200);
} finally {
await api.shutdown();
}
});
describe("the guard", () => {
it("refuses a second bootstrap, changes no password, and creates no second owner", async () => {
const url = await freshInstallation("guard");
const email = `owner-${randomBytes(4).toString("hex")}@example.test`;
const bootstrap = bootstrapAgainst(url);
try {
await bootstrap.run(email);
// Same address, different password: must not become a password reset.
await expect(bootstrap.run(email, "an entirely different password")).rejects.toMatchObject({
code: "INITIAL_OWNER_ALREADY_COMPLETED",
});
// A different address must not become a second owner.
await expect(
bootstrap.run(`second-${randomBytes(4).toString("hex")}@example.test`),
).rejects.toMatchObject({ code: "INITIAL_OWNER_ALREADY_COMPLETED" });
} finally {
await bootstrap.close();
}
expect(await countRows(url, "ekklesia_core_platform_owner")).toBe(1);
// The refused second attempt with a new address created no stray identity: the sentinel is
// checked before anything is created, not after.
expect(await countRows(url, "ekklesia_core_user")).toBe(1);
const api = await startApiWithoutSignup(url);
try {
const origin = apiOrigin(api);
expect((await signIn(origin, email, ownerPassword)).response.status).toBe(200);
expect(
(await signIn(origin, email, "an entirely different password")).response.status,
).not.toBe(200);
} finally {
await api.shutdown();
}
});
});
describe("validation reuses the identity system's own policy", () => {
it("rejects an invalid email, and a password outside the 8–128 bound", async () => {
const url = await freshInstallation("validation");
const bootstrap = bootstrapAgainst(url);
try {
await expect(bootstrap.run("not-an-email-address")).rejects.toMatchObject({
code: "INITIAL_OWNER_EMAIL_INVALID",
});
await expect(
bootstrap.run(`short-${randomBytes(4).toString("hex")}@example.test`, "1234567"),
).rejects.toMatchObject({ code: "INITIAL_OWNER_PASSWORD_TOO_SHORT" });
await expect(
bootstrap.run(`long-${randomBytes(4).toString("hex")}@example.test`, "x".repeat(129)),
).rejects.toMatchObject({ code: "INITIAL_OWNER_PASSWORD_TOO_LONG" });
// Exactly the boundary the browser accepts, so the two can never diverge.
await expect(
bootstrap.run(`edge-${randomBytes(4).toString("hex")}@example.test`, "x".repeat(128)),
).resolves.toMatchObject({ identity: "created" });
} finally {
await bootstrap.close();
}
// Every rejection happened before anything was written, and the one success is the only row.
expect(await countRows(url, "ekklesia_core_user")).toBe(1);
});
it("never puts the password into an error", async () => {
const url = await freshInstallation("nosecrets");
const secret = "sup3rSecretBootstrapPassword!";
const bootstrap = bootstrapAgainst(url);
try {
let caught: unknown;
try {
await bootstrap.run("not-an-email-address", secret);
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(InitialPlatformOwnerError);
const rendered = `${String(caught)} ${JSON.stringify(caught)} ${(caught as Error).stack ?? ""}`;
expect(rendered).not.toContain(secret);
expect(rendered).not.toContain("postgres");
expect(rendered).not.toContain("select");
} finally {
await bootstrap.close();
}
});
});
describe("an identity that already exists", () => {
it("adopts a usable account and leaves its password untouched", async () => {
const url = await freshInstallation("adopt");
const email = `owner-${randomBytes(4).toString("hex")}@example.test`;
// Exactly the partial failure this guards: the identity committed, the grant never ran.
const identity = createIdentityDrizzleDatabase(url, {});
try {
await createBootstrapIdentityCreator(identity.db)({
email,
name: "Half Finished",
password: ownerPassword,
});
} finally {
await identity.close();
}
expect(await countRows(url, "ekklesia_core_platform_owner")).toBe(0);
const bootstrap = bootstrapAgainst(url);
try {
const result = await bootstrap.run(email, "a different password than before");
expect(result.identity).toBe("adopted");
} finally {
await bootstrap.close();
}
expect(await countRows(url, "ekklesia_core_platform_owner")).toBe(1);
expect(await countRows(url, "ekklesia_core_user")).toBe(1);
const api = await startApiWithoutSignup(url);
try {
const origin = apiOrigin(api);
// Adoption granted ownership. It did not take the account over: the original password still
// works and the one supplied to the rerun does not.
expect((await signIn(origin, email, ownerPassword)).response.status).toBe(200);
expect(
(await signIn(origin, email, "a different password than before")).response.status,
).not.toBe(200);
} finally {
await api.shutdown();
}
});
it("refuses a credential-less row rather than consuming bootstrap on an unusable owner", async () => {
const url = await freshInstallation("unusable");
const client = new Client({ connectionString: url });
await client.connect();
try {
await safeQuery(
client,
`insert into "ekklesia_core_user" (id, name, email, email_verified, created_at, updated_at)
values ($1, $2, $3, false, now(), now())`,
["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "Shell Row", "[email protected]"],
);
} finally {
await client.end().catch(() => undefined);
}
const bootstrap = bootstrapAgainst(url);
try {
await expect(bootstrap.run("[email protected]")).rejects.toMatchObject({
code: "INITIAL_OWNER_IDENTITY_UNUSABLE",
});
} finally {
await bootstrap.close();
}
// The decisive part: bootstrap is still available. Granting ownership to a row nobody can
// authenticate as would have consumed the single use and left the installation unusable with
// no second chance.
expect(await countRows(url, "ekklesia_core_platform_owner_bootstrap")).toBe(0);
expect(await countRows(url, "ekklesia_core_platform_owner")).toBe(0);
});
});
describe("partial failure", () => {
it("reports a failed grant honestly and stays recoverable by rerunning", async () => {
const url = await freshInstallation("partial");
const email = `owner-${randomBytes(4).toString("hex")}@example.test`;
const authorization = createAuthorizationDrizzleDatabase(url);
const identity = createIdentityDrizzleDatabase(url, {});
try {
await expect(
createInitialPlatformOwner(
{
authorizationDb: authorization.db,
createIdentity: createBootstrapIdentityCreator(identity.db),
grantOwnership: () => {
throw new Error("the grant failed after the identity committed");
},
identityDb: identity.db,
},
{ email, name: "Owner", password: ownerPassword },
),
).rejects.toMatchObject({ code: "INITIAL_OWNER_OWNERSHIP_GRANT_FAILED" });
} finally {
await Promise.all([authorization.close(), identity.close()]);
}
// The identity committed; the grant did not; the single-use sentinel was never consumed.
expect(await countRows(url, "ekklesia_core_user")).toBe(1);
expect(await countRows(url, "ekklesia_core_platform_owner")).toBe(0);
expect(await countRows(url, "ekklesia_core_platform_owner_bootstrap")).toBe(0);
// Rerunning the identical command finishes the job, which is the whole recovery story.
const retry = bootstrapAgainst(url);
try {
await expect(retry.run(email)).resolves.toMatchObject({ identity: "adopted" });
} finally {
await retry.close();
}
expect(await countRows(url, "ekklesia_core_platform_owner")).toBe(1);
});
});
describe("concurrency", () => {
it("gives exactly one owner when several bootstraps race", async () => {
const url = await freshInstallation("race");
const attempts = 5;
const handles = Array.from({ length: attempts }, () => bootstrapAgainst(url));
try {
const outcomes = await Promise.allSettled(
handles.map((handle, index) =>
handle.run(`racer-${index}-${randomBytes(3).toString("hex")}@example.test`),
),
);
const fulfilled = outcomes.filter((outcome) => outcome.status === "fulfilled");
expect(fulfilled).toHaveLength(1);
for (const outcome of outcomes.filter((candidate) => candidate.status === "rejected")) {
expect((outcome as PromiseRejectedResult).reason).toMatchObject({
code: "INITIAL_OWNER_ALREADY_COMPLETED",
});
}
} finally {
await Promise.all(handles.map((handle) => handle.close()));
}
expect(await countRows(url, "ekklesia_core_platform_owner")).toBe(1);
expect(await countRows(url, "ekklesia_core_platform_owner_bootstrap")).toBe(1);
// The losers created nothing at all. Serializing whole invocations — not just the grant — is
// what keeps a race from leaving signable-in accounts that own nothing behind.
expect(await countRows(url, "ekklesia_core_user")).toBe(1);
});
});
describe("the command itself", () => {
it("succeeds, prints something useful, and leaks nothing", async () => {
const url = await freshInstallation("cli");
const email = `owner-${randomBytes(4).toString("hex")}@example.test`;
const result = await runBootstrapCli(url, ["--email", email], ownerPassword);
expect(result.code).toBe(0);
expect(result.stdout).toContain("Initial platform owner created successfully.");
expect(result.stdout).toContain(email.toLowerCase());
const everything = `${result.stdout} ${result.stderr}`;
expect(everything).not.toContain(ownerPassword);
expect(everything).not.toContain("postgresql://");
expect(everything).not.toContain("password");
const api = await startApiWithoutSignup(url);
try {
// The command's real product: an operator who can now sign in.
expect((await signIn(apiOrigin(api), email, ownerPassword)).response.status).toBe(200);
} finally {
await api.shutdown();
}
});
it("refuses a password supplied on the command line", async () => {
const url = await freshInstallation("argv");
const result = await runBootstrapCli(
url,
["--email", "[email protected]", "--password", "hunter2hunter2"],
undefined,
);
expect(result.code).toBe(1);
expect(result.stderr).toContain("--password is not supported");
expect(await countRows(url, "ekklesia_core_user")).toBe(0);
});
it("fails clearly without a database, and refuses to migrate one itself", async () => {
const missing = await runBootstrapCli(
undefined,
["--email", "[email protected]"],
"whatever",
);
expect(missing.code).toBe(1);
expect(missing.stderr).toContain("DATABASE_URL is required.");
// A database that exists but was never migrated is a deployment step that has not happened,
// and this command says so rather than quietly running migrations for a production operator.
const unmigrated = await createDisposableDatabase("unmigrated");
const result = await runBootstrapCli(
unmigrated,
["--email", "[email protected]"],
ownerPassword,
);
expect(result.code).toBe(1);
expect(result.stderr).toContain("INITIAL_OWNER_SCHEMA_UNAVAILABLE");
const client = new Client({ connectionString: unmigrated });
await client.connect();
try {
const tables = await safeQuery<{ count: string }>(
client,
`select count(*)::text from information_schema.tables where table_schema = 'public'`,
);
expect(Number(tables[0]?.count ?? "0")).toBe(0);
} finally {
await client.end().catch(() => undefined);
}
});
it("refuses a second run at the command level too", async () => {
const url = await freshInstallation("clirerun");
const email = `owner-${randomBytes(4).toString("hex")}@example.test`;
expect((await runBootstrapCli(url, ["--email", email], ownerPassword)).code).toBe(0);
const second = await runBootstrapCli(url, ["--email", email], "a different password");
expect(second.code).toBe(1);
expect(second.stderr).toContain("INITIAL_OWNER_ALREADY_COMPLETED");
expect(second.stderr).not.toContain("a different password");
expect(await countRows(url, "ekklesia_core_platform_owner")).toBe(1);
});
});
});