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]>
53 lines
2.9 KiB
JSON
53 lines
2.9 KiB
JSON
{
|
|
"name": "ekklesia",
|
|
"version": "0.1.0",
|
|
"private": true,
|
|
"type": "module",
|
|
"description": "Open, modular church technology platform",
|
|
"engines": {
|
|
"node": ">=24.11.0 <25",
|
|
"pnpm": ">=11.12.0 <12"
|
|
},
|
|
"packageManager": "[email protected]",
|
|
"scripts": {
|
|
"api:start": "turbo run build && node --env-file-if-exists=.env apps/api/dist/server.js",
|
|
"authorization:bootstrap-owner": "turbo run build && node --env-file-if-exists=.env core/dist/cli/bootstrap-platform-owner.js",
|
|
"boundaries": "node scripts/check-boundaries.ts",
|
|
"build": "turbo run build",
|
|
"ci:verify": "pnpm verify && pnpm infrastructure:config",
|
|
"doctor": "node scripts/run-doctor.ts",
|
|
"format": "biome format --write .",
|
|
"format:check": "biome format .",
|
|
"identity:create-initial-owner": "turbo run build && node --env-file-if-exists=.env core/dist/cli/create-initial-owner.js",
|
|
"identity:schema:validate": "turbo run build && node core/dist/cli/validate-identity-schema.js",
|
|
"infrastructure:config": "docker compose --env-file .env.example -f infrastructure/compose.yaml config --quiet",
|
|
"infrastructure:down": "docker compose --env-file .env -f infrastructure/compose.yaml down",
|
|
"infrastructure:logs": "docker compose --env-file .env -f infrastructure/compose.yaml logs --follow",
|
|
"infrastructure:smoke": "bash scripts/smoke-infrastructure.sh",
|
|
"infrastructure:up": "docker compose --env-file .env -f infrastructure/compose.yaml up --detach --wait",
|
|
"lint": "turbo run lint lint:root",
|
|
"lint:root": "biome lint .",
|
|
"migrations:apply": "turbo run build && node --env-file-if-exists=.env core/dist/cli/apply-migrations.js",
|
|
"migrations:plan": "turbo run build && node --env-file-if-exists=.env core/dist/cli/plan-migrations.js",
|
|
"migrations:validate": "turbo run build && node core/dist/cli/validate-migrations.js",
|
|
"modules:disable": "turbo run build && node --env-file-if-exists=.env core/dist/cli/set-module-enabled.js --disable",
|
|
"modules:enable": "turbo run build && node --env-file-if-exists=.env core/dist/cli/set-module-enabled.js --enable",
|
|
"modules:validate": "turbo run build && node core/dist/cli/validate-modules.js",
|
|
"test": "turbo run test test:root",
|
|
"test:migrations:integration": "pnpm build && bash scripts/test-migrations-integration.sh",
|
|
"test:root": "vitest run --config vitest.config.ts",
|
|
"test:watch": "vitest",
|
|
"typecheck": "turbo run typecheck typecheck:root",
|
|
"typecheck:root": "tsc --noEmit --project tsconfig.json",
|
|
"verify": "pnpm format:check && pnpm lint && pnpm boundaries && pnpm typecheck && pnpm test && pnpm modules:validate && pnpm migrations:validate && pnpm identity:schema:validate && pnpm build"
|
|
},
|
|
"devDependencies": {
|
|
"@biomejs/biome": "2.5.3",
|
|
"@types/node": "24.13.3",
|
|
"turbo": "2.10.4",
|
|
"typescript": "7.0.2",
|
|
"vitest": "4.1.10",
|
|
"yaml": "2.9.0"
|
|
}
|
|
}
|