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:
co-authored by
Claude Sonnet 5
parent
e296e083ec
commit
dfaf324a65
+27
-8
@@ -1,4 +1,4 @@
|
||||
import { isAuthenticatedPrincipal } from "@ekklesia/contracts";
|
||||
import { isAuthenticatedPrincipal, type RegisteredModuleHttpRoute } from "@ekklesia/contracts";
|
||||
import type {
|
||||
AuthorizationEvaluator,
|
||||
AuthorizationStore,
|
||||
@@ -9,23 +9,30 @@ import { Hono } from "hono";
|
||||
import type { Context } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { createGrantsRoutes } from "./grants-routes.js";
|
||||
import { mountModuleHttpRoutes } from "./module-http-routes.js";
|
||||
|
||||
export interface CreateApiAppAuthorizationOptions {
|
||||
readonly evaluator: AuthorizationEvaluator;
|
||||
/** The complete platform permission registry (Core's own declarations plus every installed
|
||||
* module's) — always the caller-composed registry `startApi` produces via
|
||||
* `composeStartupPermissionRegistry`, never a Core-only fallback constructed here. */
|
||||
* `prepareStartupModuleInventory`, never a Core-only fallback constructed here. */
|
||||
readonly registry: PermissionRegistry;
|
||||
readonly store: AuthorizationStore;
|
||||
}
|
||||
|
||||
export interface CreateApiAppOptions {
|
||||
readonly authorization: CreateApiAppAuthorizationOptions;
|
||||
/** The started module runtime's published, immutable route snapshot — always the caller-started
|
||||
* runtime's exact `routes` (`startDiscoveredModuleRuntime`, `apps/api/src/server.ts`), mounted in
|
||||
* publication order under `/api/v1/modules/<module-id>`. Required rather than defaulted so a
|
||||
* caller not exercising modules must say so explicitly (an empty frozen array), never rely on an
|
||||
* implicit fallback. */
|
||||
readonly moduleRoutes: readonly RegisteredModuleHttpRoute[];
|
||||
readonly runtime: EkklesiaIdentityRuntime;
|
||||
/** Exact allowed browser origins. Never a wildcard; combined with `credentials: true`, only an
|
||||
* exact, reflected match is ever returned in `Access-Control-Allow-Origin`. Also the trusted set
|
||||
* `requireTrustedOrigin` checks for grant-mutation routes — there is no second, competing origin
|
||||
* configuration. */
|
||||
* `requireTrustedOrigin` checks for grant- and module-route-mutation routes — there is no second,
|
||||
* competing origin configuration. */
|
||||
readonly trustedOrigins: readonly string[];
|
||||
}
|
||||
|
||||
@@ -36,9 +43,14 @@ function sanitizedErrorResponse(c: Context, error: unknown, status: 500 | 503) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure Hono application factory: no I/O, no environment reads, no process side effects. Suitable
|
||||
* for unit and integration tests via `app.fetch` directly, with a fully injected identity runtime
|
||||
* and authorization evaluator/store standing in for real Better Auth/PostgreSQL where appropriate.
|
||||
* Pure Hono application factory: no I/O, no environment reads, no process side effects, and it
|
||||
* never invokes a module route handler while composing routes. Suitable for unit and integration
|
||||
* tests via `app.fetch` directly, with a fully injected identity runtime and authorization
|
||||
* evaluator/store standing in for real Better Auth/PostgreSQL where appropriate. Synchronously
|
||||
* throws `ModuleRouteConfigurationError` (`./module-http-routes.js`) if `moduleRoutes` and
|
||||
* `authorization.registry` are inconsistent — a route naming a permission the registry does not
|
||||
* register, which should never happen since both derive from the same discovered inventory, but is
|
||||
* checked explicitly rather than silently mounting an unenforceable route.
|
||||
*/
|
||||
export function createApiApp(options: CreateApiAppOptions): Hono {
|
||||
const { runtime } = options;
|
||||
@@ -48,7 +60,7 @@ export function createApiApp(options: CreateApiAppOptions): Hono {
|
||||
|
||||
const corsMiddleware = cors({
|
||||
allowHeaders: ["Content-Type"],
|
||||
allowMethods: ["GET", "POST", "OPTIONS"],
|
||||
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
credentials: true,
|
||||
maxAge: 600,
|
||||
origin: (origin) => (origin && allowedOrigins.has(origin) ? origin : undefined),
|
||||
@@ -91,6 +103,13 @@ export function createApiApp(options: CreateApiAppOptions): Hono {
|
||||
}),
|
||||
);
|
||||
|
||||
mountModuleHttpRoutes(app, options.moduleRoutes, {
|
||||
evaluator: options.authorization.evaluator,
|
||||
registry: options.authorization.registry,
|
||||
runtime,
|
||||
trustedOrigins: allowedOrigins,
|
||||
});
|
||||
|
||||
app.notFound((c) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
return c.json({ code: "NOT_FOUND" }, 404);
|
||||
|
||||
Reference in New Issue
Block a user