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:
ekklesia
2026-07-23 11:16:32 -05:00
parent 72e082ff2f
commit 1363608552
33 changed files with 2962 additions and 63 deletions
+27 -14
View File
@@ -1,15 +1,15 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import {
createIdentityDrizzleDatabase,
createIdentityRuntime,
type IdentityDatabaseHandle,
type IdentityRateLimitOverride,
} from "@ekklesia/core/identity";
createAuthorizationEvaluator,
createAuthorizationStore,
} from "@ekklesia/core/authorization";
import { createIdentityRuntime, type IdentityRateLimitOverride } from "@ekklesia/core/identity";
import { serve, type ServerType } from "@hono/node-server";
import type { Hono } from "hono";
import { createApiApp } from "./app.js";
import { type ApiRuntimeConfig, createApiRuntimeConfig } from "./config.js";
import { type ApiDatabaseHandle, createApiDatabase } from "./database.js";
const connectionTimeoutMs = 10_000;
const operationTimeoutMs = 10_000;
@@ -26,7 +26,7 @@ export interface StartApiOptions {
readonly rateLimitOverride?: IdentityRateLimitOverride;
}
async function safeCloseDatabase(handle: IdentityDatabaseHandle): Promise<void> {
async function safeCloseDatabase(handle: ApiDatabaseHandle): Promise<void> {
try {
await handle.close();
} catch {
@@ -54,34 +54,47 @@ function bindServer(app: Hono, host: string, port: number): Promise<ServerType>
}
/**
* Explicit runtime composition: constructs the PostgreSQL pool and Drizzle database, the Core
* identity runtime, and the Hono application, then starts the Node HTTP server. Returns a handle
* with an idempotent `shutdown()` so both the executable entry point and tests can stop the
* server and close the database pool deterministically. Never applies migrations or generates
* schema — the database must already be migrated through `pnpm migrations:apply`.
* Explicit runtime composition: constructs the one shared PostgreSQL pool (identity and
* authorization each get a typed Drizzle view over it — see `./database.js`), the Core identity
* runtime, the Core authorization store and evaluator, and the Hono application, then starts the
* Node HTTP server. Returns a handle with an idempotent `shutdown()` so both the executable entry
* point and tests can stop the server and close the shared pool deterministically. Never applies
* migrations or generates schema — the database must already be migrated through
* `pnpm migrations:apply`.
*/
export async function startApi(
config: ApiRuntimeConfig,
options: StartApiOptions = {},
): Promise<RunningApi> {
const databaseHandle = createIdentityDrizzleDatabase(config.databaseUrl, {
const databaseHandle = createApiDatabase(config.databaseUrl, {
connectionTimeoutMs,
operationTimeoutMs,
});
let runtime: ReturnType<typeof createIdentityRuntime>;
let authorizationStore: ReturnType<typeof createAuthorizationStore>;
try {
runtime = createIdentityRuntime({
config: config.identity,
db: databaseHandle.db,
db: databaseHandle.identity,
...(options.rateLimitOverride ? { rateLimitOverride: options.rateLimitOverride } : {}),
});
authorizationStore = createAuthorizationStore(databaseHandle.authorization);
} catch (error) {
await safeCloseDatabase(databaseHandle);
throw error;
}
const app = createApiApp({ runtime, trustedOrigins: config.identity.trustedOrigins });
const evaluator = createAuthorizationEvaluator({
grants: authorizationStore,
platformOwners: authorizationStore,
});
const app = createApiApp({
authorization: { evaluator, store: authorizationStore },
runtime,
trustedOrigins: config.identity.trustedOrigins,
});
let server: ServerType;
try {