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
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:
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -124,7 +124,7 @@ async function createDisplayModuleDirectory(): Promise<string> {
|
||||
server: "./dist/server/index.js",
|
||||
},
|
||||
id: "announcements",
|
||||
manifestVersion: 7,
|
||||
manifestVersion: 8,
|
||||
name: "Announcements fixture",
|
||||
permissions: [
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture = {
|
||||
: {}),
|
||||
},
|
||||
id,
|
||||
manifestVersion: 7,
|
||||
manifestVersion: 8,
|
||||
name: `${id} module`,
|
||||
permissions: fixture.display
|
||||
? [
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -129,7 +129,7 @@ async function createFixtureModule(
|
||||
: {}),
|
||||
},
|
||||
id,
|
||||
manifestVersion: 7,
|
||||
manifestVersion: 8,
|
||||
name,
|
||||
permissions: [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user