feat: mount module HTTP routes

Phase 5B, pass 2: production API mounting, authorization enforcement,
and module-runtime lifecycle integration.

apps/api now discovers the installed module inventory exactly once
(prepareStartupModuleInventory), composes the platform permission
registry from it, starts the module runtime from that same discovered
list (startDiscoveredModuleRuntime, never rediscovering), and mounts
StartedModuleRuntime's published routes under
/api/v1/modules/<module-id> via a new apps/api-owned Hono adapter
(module-http-routes.ts). Every module route is protected by the
existing requirePermission middleware against the composed registry,
state-changing methods additionally require a trusted Origin, OPTIONS
remains entirely platform-owned, and a handler that throws, rejects,
or returns a non-Response becomes a fixed, sanitized
500 MODULE_HANDLER_FAILED. Module-supplied CORS headers are stripped
and every module response is forced to Cache-Control: no-store with
Vary: Origin, Cookie. RunningApi shutdown now stops the module runtime
between draining the HTTP server and closing the shared pool, and
every startup failure path stops the runtime and/or closes the pool
before propagating, without masking the original failure.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
ekklesia
2026-07-24 19:16:20 -05:00
co-authored by Claude Sonnet 5
parent e296e083ec
commit dfaf324a65
12 changed files with 1930 additions and 123 deletions
+15
View File
@@ -52,6 +52,7 @@ describe("Ekklesia API Hono application", () => {
it("mounts Better Auth exactly under /api/auth/*", async () => {
const runtime = fakeRuntime();
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime,
trustedOrigins: [trustedOrigin],
@@ -67,6 +68,7 @@ describe("Ekklesia API Hono application", () => {
it("does not route requests outside /api/auth/* to Better Auth", async () => {
const runtime = fakeRuntime();
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime,
trustedOrigins: [trustedOrigin],
@@ -83,6 +85,7 @@ describe("Ekklesia API Hono application", () => {
resolvePrincipal: vi.fn().mockResolvedValue(ANONYMOUS_PRINCIPAL),
});
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime,
trustedOrigins: [trustedOrigin],
@@ -101,6 +104,7 @@ describe("Ekklesia API Hono application", () => {
resolvePrincipal: vi.fn().mockResolvedValue(createAuthenticatedPrincipal("user_123")),
});
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime,
trustedOrigins: [trustedOrigin],
@@ -121,6 +125,7 @@ describe("Ekklesia API Hono application", () => {
.mockRejectedValue(new IdentityRuntimeError("PRINCIPAL_RESOLUTION_FAILED")),
});
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime,
trustedOrigins: [trustedOrigin],
@@ -138,6 +143,7 @@ describe("Ekklesia API Hono application", () => {
handleAuthRequest: vi.fn().mockRejectedValue(new Error("postgresql://admin:secret@db/x")),
});
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime,
trustedOrigins: [trustedOrigin],
@@ -154,6 +160,7 @@ describe("Ekklesia API Hono application", () => {
it("has deterministic 404 behavior for an unknown route", async () => {
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime: fakeRuntime(),
trustedOrigins: [trustedOrigin],
@@ -167,6 +174,7 @@ describe("Ekklesia API Hono application", () => {
it("reflects an allowed origin in Access-Control-Allow-Origin with credentials support", async () => {
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime: fakeRuntime(),
trustedOrigins: [trustedOrigin],
@@ -182,6 +190,7 @@ describe("Ekklesia API Hono application", () => {
it("never returns Access-Control-Allow-Origin for a disallowed origin", async () => {
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime: fakeRuntime(),
trustedOrigins: [trustedOrigin],
@@ -196,6 +205,7 @@ describe("Ekklesia API Hono application", () => {
it("supports requests with no Origin header at all (native/server clients)", async () => {
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime: fakeRuntime(),
trustedOrigins: [trustedOrigin],
@@ -208,6 +218,7 @@ describe("Ekklesia API Hono application", () => {
it("handles preflight OPTIONS requests deterministically for an allowed origin", async () => {
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime: fakeRuntime(),
trustedOrigins: [trustedOrigin],
@@ -227,6 +238,7 @@ describe("Ekklesia API Hono application", () => {
it("never wildcards Access-Control-Allow-Origin", async () => {
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime: fakeRuntime(),
trustedOrigins: [trustedOrigin],
@@ -249,6 +261,7 @@ describe("Ekklesia API Hono application", () => {
it("mounts the v1 grants routes under /api/v1/grants", async () => {
const evaluatorAuthorize = vi.fn().mockResolvedValue({ allowed: false });
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({ evaluator: { authorize: evaluatorAuthorize } }),
runtime: fakeRuntime(),
trustedOrigins: [trustedOrigin],
@@ -274,6 +287,7 @@ describe("Ekklesia API Hono application", () => {
has: () => false,
};
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({
evaluator: { authorize: evaluatorAuthorize },
registry: emptyRegistry,
@@ -290,6 +304,7 @@ describe("Ekklesia API Hono application", () => {
it("applies the same CORS policy to /api/v1/grants as the rest of /api/v1/*", async () => {
const app = createApiApp({
moduleRoutes: Object.freeze([]),
authorization: fakeAuthorization({}),
runtime: fakeRuntime(),
trustedOrigins: [trustedOrigin],