Every PostgreSQL connection this repository opened was plaintext, in every environment: no connection site set `ssl` at all, so credentials, session tokens, and row data crossed the network in the clear and nothing in configuration, startup, or `pnpm doctor` said so. The public identity URL boundary was also looser than it appeared. `core/src/database/transport.ts` is now the one typed policy. It has exactly two modes: `verify-full` (TLS required, certificate chain verified, and server identity verified against the configured host — the production default) and `disable` (the development/test default, refused outright in production). There is deliberately no encryption-only `require` mode, no chain-only `verify-ca`, and no `rejectUnauthorized: false` escape hatch anywhere in the configuration surface; every loose or aliased spelling is rejected in both environments, so a typo never quietly becomes a weaker mode. Production has no plaintext exception at all, not even for a loopback database — a connection string cannot prove the database is process-local. Verification is certificate *and* hostname. The policy supplies an explicit `checkServerIdentity` pinned to the host parsed from DATABASE_URL, because the pinned driver sets TLS `servername` only for non-IP hosts and Node performs no identity check when it is unset — so `rejectUnauthorized: true` alone accepts a certificate from any host the trusted CA ever signed when connecting by IP. Both halves are proven against a real TLS-enabled PostgreSQL, including that the same connection without the pinned callback succeeds. Every CA, certificate, and key is generated at test runtime and deleted afterwards. A DATABASE_URL may no longer carry its own TLS parameters. The pinned driver merges parsed connection-string values over explicitly passed options, so a `?sslmode=disable` in the URL would silently replace the policy's verified `ssl` object; that now fails with a stable code, reported by both `pnpm doctor` and the migration CLIs rather than as "failed unexpectedly". One shared environment reader keeps `process.env` out of Core's reusable constructors, and every executable that opens a connection resolves the policy identically: the API (validated before any pool, Better Auth instance, module runtime, or listener exists), both migration commands, the bootstrap CLI, and Doctor's own connection. Doctor reports it as a blocking failure, never a warning, naming only the effective mode. Separately, the identity base URL and every trusted origin must now be exactly one canonical origin — the WHATWG origin serialization must equal the supplied string — so a path, trailing slash, credentials, explicit default port, or non-canonical case is rejected rather than normalized into something that no longer matches what a browser sends. Production's loopback rejection now classifies the address instead of comparing against three literals, covering `.localhost`, all of 127.0.0.0/8, 0.0.0.0, `::`, expanded and IPv4-mapped forms. This is a configuration-shape guarantee only: it does not resolve DNS or prove reachability, and TLS protects the connection, not the exposure. Co-Authored-By: Claude Opus 5 <[email protected]>
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { safeParseEkklesiaUserId } from "@ekklesia/contracts";
|
|
import {
|
|
bootstrapPlatformOwner,
|
|
createAuthorizationDrizzleDatabase,
|
|
PlatformOwnerBootstrapError,
|
|
} from "../authorization/index.js";
|
|
import { resolveExecutableDatabaseTransport } from "./database-transport-environment.js";
|
|
|
|
class BootstrapCliError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "BootstrapCliError";
|
|
}
|
|
}
|
|
|
|
const usage =
|
|
"Usage: pnpm authorization:bootstrap-owner -- --user-id <id> --confirm-user-id <same-id>";
|
|
|
|
interface ParsedArgs {
|
|
readonly confirmUserId: string;
|
|
readonly userId: string;
|
|
}
|
|
|
|
/** A minimal, hand-rolled flag parser — this command takes exactly two required, named flags, so
|
|
* a general-purpose argument-parsing dependency is not warranted. */
|
|
function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
let userId: string | undefined;
|
|
let confirmUserId: string | undefined;
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const flag = argv[index];
|
|
if (flag === "--user-id") {
|
|
userId = argv[index + 1];
|
|
index += 1;
|
|
} else if (flag === "--confirm-user-id") {
|
|
confirmUserId = argv[index + 1];
|
|
index += 1;
|
|
}
|
|
}
|
|
if (userId === undefined || confirmUserId === undefined) {
|
|
throw new BootstrapCliError(usage);
|
|
}
|
|
return { confirmUserId, userId };
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const { confirmUserId, userId: rawUserId } = parseArgs(process.argv.slice(2));
|
|
|
|
// Deliberately not "infer the first user" or "select by signup order" — the operator must name
|
|
// the exact, opaque Ekklesia user id twice, and both must match exactly, before anything else
|
|
// is even attempted.
|
|
if (rawUserId !== confirmUserId) {
|
|
throw new BootstrapCliError(
|
|
"--user-id and --confirm-user-id must match exactly. No changes were made.",
|
|
);
|
|
}
|
|
|
|
const parsedUserId = safeParseEkklesiaUserId(rawUserId);
|
|
if (!parsedUserId.success) {
|
|
throw new BootstrapCliError("The provided user id is not a valid Ekklesia user id.");
|
|
}
|
|
|
|
// Read only when this command actually executes — never merely by importing this module (see
|
|
// isDirectExecution() below) or any module it imports.
|
|
const databaseUrl = process.env.DATABASE_URL;
|
|
if (typeof databaseUrl !== "string" || databaseUrl.trim().length === 0) {
|
|
throw new BootstrapCliError("DATABASE_URL is required.");
|
|
}
|
|
|
|
// Asserted before the connection is constructed, so an unsafe production transport never opens
|
|
// a client at all.
|
|
const transport = resolveExecutableDatabaseTransport(databaseUrl);
|
|
const { close, db } = createAuthorizationDrizzleDatabase(databaseUrl, {
|
|
connectionTimeoutMs: 10_000,
|
|
operationTimeoutMs: 10_000,
|
|
transport,
|
|
});
|
|
try {
|
|
await bootstrapPlatformOwner(db, { userId: parsedUserId.data });
|
|
console.log(`Platform-owner bootstrap complete for user "${parsedUserId.data}".`);
|
|
} finally {
|
|
await close().catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
function reportBootstrapCliError(error: unknown): void {
|
|
if (error instanceof BootstrapCliError) {
|
|
console.error(error.message);
|
|
return;
|
|
}
|
|
if (error instanceof PlatformOwnerBootstrapError) {
|
|
console.error(`[${error.code}] ${error.message}`);
|
|
return;
|
|
}
|
|
console.error("Platform-owner bootstrap failed unexpectedly.");
|
|
}
|
|
|
|
function isDirectExecution(): boolean {
|
|
const entrypoint = process.argv[1];
|
|
return (
|
|
entrypoint !== undefined && import.meta.url === pathToFileURL(path.resolve(entrypoint)).href
|
|
);
|
|
}
|
|
|
|
if (isDirectExecution()) {
|
|
main().catch((error: unknown) => {
|
|
reportBootstrapCliError(error);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|