feat: add authorization bootstrap and HTTP enforcement
Wires Core's identity and authorization domains into apps/api over one shared, validated PostgreSQL pool; adds an explicit, single-use, concurrency-safe platform-owner bootstrap CLI; adds a smallest-possible immutable permission registry with one Core-owned restricted permission (core.authorization.grants.manage); and adds reusable Hono authorization enforcement plus a narrowly scoped /api/v1/grants HTTP API for managing explicit grants only, gated by an exact trusted Origin on every mutation.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { safeParseEkklesiaUserId } from "@ekklesia/contracts";
|
||||
import {
|
||||
bootstrapPlatformOwner,
|
||||
createAuthorizationDrizzleDatabase,
|
||||
PlatformOwnerBootstrapError,
|
||||
} from "../authorization/index.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.");
|
||||
}
|
||||
|
||||
const { close, db } = createAuthorizationDrizzleDatabase(databaseUrl, {
|
||||
connectionTimeoutMs: 10_000,
|
||||
operationTimeoutMs: 10_000,
|
||||
});
|
||||
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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user