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
+3 -3
View File
@@ -342,7 +342,7 @@ async function createGatedRouteModule(
compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" },
entrypoints: { server: "./dist/server/index.js" },
id,
manifestVersion: 7,
manifestVersion: 8,
name: `${id} module`,
permissions: [{ baseline: "public", description: "Gated drain probe.", id: permissionId }],
version: "0.1.0",
@@ -386,7 +386,7 @@ async function createFixtureModule(
): Promise<void> {
const moduleDirectory = path.join(modulesDirectory, id);
const manifest = {
manifestVersion: 7,
manifestVersion: 8,
id,
name: `${id} module`,
version: "0.1.0",
@@ -524,7 +524,7 @@ async function createUiModule(
compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" },
entrypoints: { server: "./dist/server/index.js", ui: "./dist/ui/index.js" },
id,
manifestVersion: 7,
manifestVersion: 8,
name: `${id} module`,
version,
}),
+1 -1
View File
@@ -77,7 +77,7 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture): P
...(fixture.withUi === false ? {} : { ui: "./dist/ui/index.js" }),
},
id: fixture.id,
manifestVersion: 7,
manifestVersion: 8,
name: fixture.name,
version: "1.0.0",
}),
+1 -1
View File
@@ -124,7 +124,7 @@ async function createDisplayModuleDirectory(): Promise<string> {
server: "./dist/server/index.js",
},
id: "announcements",
manifestVersion: 7,
manifestVersion: 8,
name: "Announcements fixture",
permissions: [
{
+2 -2
View File
@@ -104,7 +104,7 @@ async function createFixtureModule(
): Promise<void> {
const moduleDirectory = path.join(modulesDirectory, id);
const manifest = {
manifestVersion: 7,
manifestVersion: 8,
id,
name: `${id} module`,
version: "0.1.0",
@@ -270,7 +270,7 @@ function fakeDiscoveredModule(
compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" },
entrypoints: { server: "./dist/does-not-exist/index.js" },
id,
manifestVersion: 7,
manifestVersion: 8,
name: `${id} module`,
permissions,
version: "0.1.0",
@@ -108,7 +108,7 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture): P
configuration: fixture.configuration ?? [],
entrypoints: { server: "./dist/server/index.js" },
id: fixture.id,
manifestVersion: 7,
manifestVersion: 8,
name: `${fixture.id} module`,
version: "1.0.0",
}),
@@ -8,6 +8,7 @@ import {
createPostgresMigrationDatabase,
discoverAllMigrations,
} from "@ekklesia/core/migrations";
import { moduleDatabaseExecutorModuleId } from "@ekklesia/core/modules";
import { Client, type QueryResultRow } from "pg";
import { afterAll, describe, expect, it } from "vitest";
import { createApiRuntimeConfig } from "../src/config.js";
@@ -441,6 +442,11 @@ describe.sequential("live API — module database explicit migration ownership",
const handle = createApiDatabase(database.url);
expect(handle.module).toBeDefined();
const alpha = handle.moduleExecutor("alpha");
const beta = handle.moduleExecutor("beta");
expect(alpha).not.toBe(beta);
expect(moduleDatabaseExecutorModuleId(alpha)).toBe("alpha");
expect(moduleDatabaseExecutorModuleId(beta)).toBe("beta");
await handle.close();
expect(await tableExists(database.url)).toBe(false);
+1 -1
View File
@@ -105,7 +105,7 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture = {
: {}),
},
id,
manifestVersion: 7,
manifestVersion: 8,
name: `${id} module`,
permissions: fixture.display
? [
+41 -10
View File
@@ -6,8 +6,8 @@ import {
type ModuleRouteHandlerContext,
type ModuleRuntimeServices,
moduleRoutesOverlap,
parsePermissionId,
type PermissionId,
parsePermissionId,
type RegisteredModuleHttpRoute,
} from "@ekklesia/contracts";
import {
@@ -20,20 +20,21 @@ import type { EkklesiaIdentityRuntime } from "@ekklesia/core/identity";
import {
createModuleUiPublicationRegistry,
EMPTY_MODULE_REALTIME_REGISTRY,
type ModuleRuntimeServicesLookup,
} from "@ekklesia/core/modules";
import { describe, expect, it, vi } from "vitest";
import { createInMemoryDashboardStore } from "./support/in-memory-dashboard-store.js";
import { testDashboardHostAssets } from "./support/dashboard-host-assets.js";
import { readyHealth } from "./support/ready-health.js";
import { createApiApp } from "../src/app.js";
import {
canonicalizeVaryHeader,
mergedVaryHeader,
ModuleRouteConfigurationError,
mergedVaryHeader,
} from "../src/module-http-routes.js";
import { emptyModuleLifecycle } from "./support/in-memory-module-lifecycle.js";
import { testDashboardHostAssets } from "./support/dashboard-host-assets.js";
import { createInMemoryDashboardStore } from "./support/in-memory-dashboard-store.js";
import { emptyModuleConfiguration } from "./support/in-memory-module-configuration.js";
import { emptyModuleLifecycle } from "./support/in-memory-module-lifecycle.js";
import { emptyModuleStorage } from "./support/in-memory-module-storage.js";
import { readyHealth } from "./support/ready-health.js";
const trustedOrigin = "https://dashboard.example.test";
const untrustedOrigin = "https://evil.example.test";
@@ -107,7 +108,7 @@ function buildModuleApp(options: {
readonly registry: PermissionRegistry;
readonly routes: readonly RegisteredModuleHttpRoute[];
readonly runtime?: EkklesiaIdentityRuntime;
readonly services?: ModuleRuntimeServices;
readonly services?: ModuleRuntimeServices | ModuleRuntimeServicesLookup;
readonly trustedOrigins?: readonly string[];
}) {
const evaluator = options.evaluator ?? {
@@ -145,6 +146,36 @@ function permissionRegistry(
}
describe("module HTTP route mounting", () => {
it("selects the owning module's stable service entry for its handler", async () => {
const calendar = fakeModuleServices();
const people = fakeModuleServices();
const services: ModuleRuntimeServicesLookup = new Map([
["calendar", calendar],
["people", people],
]);
let received: unknown;
const { app } = buildModuleApp({
registry: permissionRegistry([{ id: readPermission, baseline: "public" }]),
routes: [
moduleRoute({
handler: (_request, context) => {
received = context.services;
return Response.json({ ok: true });
},
method: "GET",
moduleId: "calendar",
path: "/services",
permission: readPermission,
}),
],
services,
});
expect((await app.request("/api/v1/modules/calendar/services")).status).toBe(200);
expect(received).toBe(calendar);
expect(received).not.toBe(people);
});
it('mounts a "/" route at exactly the module root, never with an added trailing slash', async () => {
const handler = vi.fn<ModuleRouteHandler>(async () => new Response("root", { status: 200 }));
const { app } = buildModuleApp({
@@ -508,7 +539,7 @@ describe("module route authorization", () => {
expect(response.status).toBe(403);
expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" });
expect(handler).not.toHaveBeenCalled();
expectDatabaseFakeUntouched(services);
expectDatabaseFakeUntouched(services as ModuleRuntimeServices);
});
it("does not invoke the handler when principal resolution fails", async () => {
@@ -637,7 +668,7 @@ describe("module route trusted-origin enforcement", () => {
}
expect(handler).not.toHaveBeenCalled();
expect(evaluator.authorize).not.toHaveBeenCalled();
expectDatabaseFakeUntouched(services);
expectDatabaseFakeUntouched(services as ModuleRuntimeServices);
});
it("allows a trusted-origin state-changing request through to the handler", async () => {
@@ -766,7 +797,7 @@ describe("module route CORS", () => {
expect(handler).not.toHaveBeenCalled();
expect(evaluator.authorize).not.toHaveBeenCalled();
expectDatabaseFakeUntouched(services);
expectDatabaseFakeUntouched(services as ModuleRuntimeServices);
});
it("never reflects an untrusted origin for a module route", async () => {
@@ -79,7 +79,7 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture): P
...(fixture.dependencies ? { dependencies: fixture.dependencies } : {}),
entrypoints: { server: "./dist/server/index.js", ui: "./dist/ui/index.js" },
id: fixture.id,
manifestVersion: 7,
manifestVersion: 8,
name: `${fixture.id} module`,
version: "1.0.0",
}),
+1 -1
View File
@@ -188,7 +188,7 @@ async function createFixtureModule(control: FixtureControl): Promise<{
compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" },
entrypoints: { server: "./dist/server/index.js" },
id: "announcements",
manifestVersion: 7,
manifestVersion: 8,
name: "Announcements fixture",
permissions: [
{
+1 -1
View File
@@ -129,7 +129,7 @@ async function createFixtureModule(
: {}),
},
id,
manifestVersion: 7,
manifestVersion: 8,
name,
permissions: [
{
+1 -1
View File
@@ -99,7 +99,7 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture): P
version: "1.0.0",
})),
id: fixture.id,
manifestVersion: 7,
manifestVersion: 8,
name: `${fixture.id} module`,
permissions: (fixture.permissions ?? []).map((permission) => ({
...permission,
+1 -1
View File
@@ -139,7 +139,7 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture): P
version: "1.0.0",
})),
id: fixture.id,
manifestVersion: 7,
manifestVersion: 8,
name: `${fixture.id} module`,
permissions:
fixture.display === undefined