feat: deliver module database ABI by manifest
CI / verify (pull_request) Successful in 1m0s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 23s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s

This commit is contained in:
ekklesia
2026-10-07 17:29:32 -05:00
parent a1cdcb3ad1
commit 49b4d4165d
60 changed files with 948 additions and 237 deletions
+23 -13
View File
@@ -2,7 +2,8 @@ import { z } from "zod";
import { type ModuleId, type PermissionId, permissionIdSchema } from "./identifiers.js";
import type { EkklesiaPrincipal } from "./identity.js";
import type { ModuleConfiguration } from "./module-configuration.js";
import type { ModuleRuntimeServices } from "./module-database.js";
import type { ModuleDatabase, ModuleRuntimeServices } from "./module-database.js";
import type { ModuleDatabaseExecutor } from "./module-database-abi.js";
import type { ModuleRealtimePublisher } from "./module-realtime.js";
import type { ModuleStorage } from "./module-storage.js";
@@ -271,7 +272,9 @@ export function moduleRoutesOverlap(
* behavior, and uncaught-handler sanitization; permission enforcement always completes before a
* handler — and therefore any parameter validation it performs — can run.
*/
export interface ModuleRouteHandlerContext {
export interface ModuleRouteHandlerContext<
Database extends ModuleDatabase | ModuleDatabaseExecutor = ModuleDatabase,
> {
readonly moduleId: ModuleId;
readonly principal: EkklesiaPrincipal;
readonly params: Readonly<Record<string, string>>;
@@ -279,7 +282,7 @@ export interface ModuleRouteHandlerContext {
* object, not a re-resolved one (ADR 0030). This is what lets a module stream an object straight
* into a `Response` without the browser ever learning that object storage exists. */
readonly storage: ModuleStorage;
readonly services: ModuleRuntimeServices;
readonly services: ModuleRuntimeServices<Database>;
/** The identical module-scoped realtime publisher used by lifecycle hooks. */
readonly realtime: ModuleRealtimePublisher;
/** The identical frozen configuration snapshot this module's `start`/`stop` hooks received — the
@@ -293,10 +296,9 @@ export interface ModuleRouteHandlerContext {
/** A framework-neutral Fetch handler: standard `Request` in, standard `Response` out. The full
* query string remains available through `Request.url`; there is no competing query or body
* abstraction. */
export type ModuleRouteHandler = (
request: Request,
context: ModuleRouteHandlerContext,
) => Awaitable<Response>;
export type ModuleRouteHandler<
Database extends ModuleDatabase | ModuleDatabaseExecutor = ModuleDatabase,
> = (request: Request, context: ModuleRouteHandlerContext<Database>) => Awaitable<Response>;
/**
* What a module passes to `http.route(...)` during `register`. `permission` is a plain string here
@@ -311,7 +313,9 @@ export type ModuleRouteHandler = (
* path, CORS/origin policy, authentication behavior, grant/platform-owner state, middleware, audit
* metadata, another module's namespace, or an unprotected/no-permission route.
*/
export interface ModuleRouteDefinitionInput {
export interface ModuleRouteDefinitionInput<
Database extends ModuleDatabase | ModuleDatabaseExecutor = ModuleDatabase,
> {
readonly method: ModuleHttpMethod;
readonly path: string;
readonly permission: string;
@@ -322,12 +326,14 @@ export interface ModuleRouteDefinitionInput {
* existed meant, so adding this field changed no existing route's behaviour.
*/
readonly bodyMode?: ModuleRouteBodyMode;
readonly handler: ModuleRouteHandler;
readonly handler: ModuleRouteHandler<Database>;
}
/** The validated, canonical form of a `ModuleRouteDefinitionInput` — what `http.route` produces
* only after every field passes its own grammar. */
export interface ModuleRouteDefinition {
export interface ModuleRouteDefinition<
Database extends ModuleDatabase | ModuleDatabaseExecutor = ModuleDatabase,
> {
readonly method: ModuleHttpMethod;
readonly path: ModuleRoutePath;
readonly permission: PermissionId;
@@ -335,13 +341,15 @@ export interface ModuleRouteDefinition {
/** Always present in the canonical form, defaulted to `buffered`, so no consumer downstream has
* to re-apply the default and none of them can disagree about it. */
readonly bodyMode: ModuleRouteBodyMode;
readonly handler: ModuleRouteHandler;
readonly handler: ModuleRouteHandler<Database>;
}
/** The Fetch-based registrar exposed as `context.http` during `register` only — never on
* `ModuleRuntimeContext`, so `start`/`stop` cannot register a route after the fact. */
export interface ModuleHttpRouteRegistrar {
route(definition: ModuleRouteDefinitionInput): void;
export interface ModuleHttpRouteRegistrar<
Database extends ModuleDatabase | ModuleDatabaseExecutor = ModuleDatabase,
> {
route(definition: ModuleRouteDefinitionInput<Database>): void;
}
/** One published route in `StartedModuleRuntime`'s immutable route snapshot — the same validated
@@ -355,6 +363,8 @@ export interface RegisteredModuleHttpRoute {
readonly permission: PermissionId;
readonly maxBodyBytes: number;
readonly bodyMode: ModuleRouteBodyMode;
/** Runtime-erased callback. Module authors get the database-mode-specific handler type through
* `ModuleHttpRouteRegistrar`; Core stores the validated callback opaquely in this snapshot. */
readonly handler: ModuleRouteHandler;
}