import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { ANONYMOUS_PRINCIPAL, createAuthenticatedPrincipal, type EkklesiaPrincipal, type ModuleConfigurationDeclarationInput, type ModuleDatabase, type ModuleRuntimeServices, parseEkklesiaUserId, } from "@ekklesia/contracts"; import { type AuthorizationStore, CORE_MODULES_CONFIGURATION_MANAGE, CORE_MODULES_LIFECYCLE_MANAGE, createAuthorizationEvaluator, } from "@ekklesia/core/authorization"; import type { EkklesiaIdentityRuntime } from "@ekklesia/core/identity"; import { composePlatformPermissionRegistry, createModuleUiPublicationRegistry, type DiscoveredModule, discoverModules, type ModuleConfigurationRecord, type ModuleConfigurationStore, ModuleConfigurationStoreError, EMPTY_MODULE_REALTIME_REGISTRY, resolveModuleEligibility, } from "@ekklesia/core/modules"; import type { Hono } from "hono"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createApiApp } from "../src/app.js"; import { CORE_MUTATION_MAX_BODY_BYTES } from "../src/body-limits.js"; import { MODULE_CONFIGURATION_PATH } from "../src/module-configuration-routes.js"; import { MODULE_LIFECYCLE_PATH } from "../src/module-lifecycle-routes.js"; import { createInMemoryDashboardStore } from "./support/in-memory-dashboard-store.js"; import { readyHealth } from "./support/ready-health.js"; import { createFakeModuleSecretSource, createInMemoryModuleConfigurationStore, } from "./support/in-memory-module-configuration.js"; import { createInMemoryModuleLifecycleStore } from "./support/in-memory-module-lifecycle.js"; import { moduleStorageOptionFor } from "./support/in-memory-module-storage.js"; const trustedOrigin = "https://dashboard.example.test"; const untrustedOrigin = "https://evil.example.test"; const viewerId = parseEkklesiaUserId("55555555-5555-4555-8555-555555555555"); const temporaryDirectories: string[] = []; afterEach(async () => { await Promise.all( temporaryDirectories .splice(0) .map((directory) => rm(directory, { force: true, recursive: true })), ); }); /** * The configuration shape the real Announcements product would need as an Ekklesia module: a slide * duration and poll interval with defaults, a bucket name it must be told, and a deployment-supplied * storage credential. * * Modelled from the reference repository, never imported from it. Nothing Announcements-specific * exists in Core or in `apps/api`; this fixture exists to prove a real product's settings survive * the whole path — manifest, validation, storage, API, snapshot — without the module reaching any * platform infrastructure. */ const announcementsConfiguration: readonly ModuleConfigurationDeclarationInput[] = [ { key: "slideSeconds", type: "integer", default: 7, minimum: 1, maximum: 600, description: "How long each slide is shown.", }, { key: "pollIntervalSeconds", type: "integer", default: 30, minimum: 5, maximum: 3600 }, { key: "bucket", type: "string", required: true, minLength: 1, maxLength: 63 }, { key: "fit", type: "enum", values: ["contain", "cover"], default: "contain" }, { key: "storageCredential", type: "secret", required: true }, ]; interface ModuleFixture { readonly id: string; readonly configuration?: readonly ModuleConfigurationDeclarationInput[]; } async function createModule(modulesDirectory: string, fixture: ModuleFixture): Promise { const moduleDirectory = path.join(modulesDirectory, fixture.id); await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); await Promise.all([ writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify({ ekklesia: { layer: "module" }, name: `@test/module-${fixture.id}`, type: "module", version: "1.0.0", }), ), writeFile( path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify({ compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" }, configuration: fixture.configuration ?? [], entrypoints: { server: "./dist/server/index.js" }, id: fixture.id, manifestVersion: 7, name: `${fixture.id} module`, version: "1.0.0", }), ), writeFile( path.join(moduleDirectory, "dist/server/index.js"), `export default { id: ${JSON.stringify(fixture.id)}, version: "1.0.0" };`, ), ]); } interface InstallOptions { readonly fixtures?: readonly ModuleFixture[]; readonly principal?: EkklesiaPrincipal; readonly platformOwner?: boolean; readonly grants?: readonly string[]; readonly records?: readonly ModuleConfigurationRecord[]; readonly secrets?: Readonly>>>; readonly store?: ModuleConfigurationStore; } interface Installation { readonly app: Hono; readonly modules: readonly DiscoveredModule[]; readonly store: ModuleConfigurationStore; } /** * Composes the real application over a real discovered inventory, reproducing `startApi`'s own * composition: read the stored settings, resolve eligibility and configuration in one pass, then * build the UI registry and every option from that same resolution. Doing it the same way here is * what makes these assertions evidence about production composition rather than a bespoke wiring. */ async function install(options: InstallOptions = {}): Promise { const modulesDirectory = await mkdtemp(path.join(tmpdir(), "ekklesia-module-config-")); temporaryDirectories.push(modulesDirectory); for (const fixture of options.fixtures ?? [ { configuration: announcementsConfiguration, id: "announcements" }, ]) { await createModule(modulesDirectory, fixture); } const modules = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); const store = options.store ?? createInMemoryModuleConfigurationStore(options.records ?? []); const secrets = createFakeModuleSecretSource(options.secrets ?? {}); const lifecycleStore = createInMemoryModuleLifecycleStore(); const bootSettings = await store.readSettings(); const eligibility = resolveModuleEligibility(modules, await lifecycleStore.readConfiguration(), { secrets, settings: bootSettings, }); const held = new Set(options.grants ?? []); const evaluator = createAuthorizationEvaluator({ grants: { hasActiveGrant: async (query) => held.has(String(query.permission)) }, platformOwners: { isPlatformOwner: async () => options.platformOwner === true }, }); const runtime: EkklesiaIdentityRuntime = { handleAuthRequest: vi.fn().mockResolvedValue(new Response(null, { status: 200 })), resolvePrincipal: vi.fn().mockResolvedValue(options.principal ?? ANONYMOUS_PRINCIPAL), }; const authorizationStore = { createGrant: vi.fn(), grantPlatformOwner: vi.fn(), hasActiveGrant: vi.fn().mockResolvedValue(false), isPlatformOwner: vi.fn().mockResolvedValue(false), listGrantsForUser: vi.fn().mockResolvedValue({ grants: [] }), revokeGrant: vi.fn(), revokePlatformOwner: vi.fn(), } as unknown as AuthorizationStore; const app = createApiApp({ authorization: { evaluator, registry: composePlatformPermissionRegistry(modules), store: authorizationStore, }, dashboard: createInMemoryDashboardStore(), health: readyHealth(), moduleConfiguration: { bootSettings, discoveredModules: modules, secrets, snapshots: eligibility.configurations, store, }, moduleStorage: moduleStorageOptionFor(modules), moduleLifecycle: { snapshot: { discoveredModules: modules, eligibility: eligibility.entries, startedModuleIds: eligibility.eligibleModules.map((module) => module.manifest.id), }, store: lifecycleStore, }, moduleRoutes: Object.freeze([]), moduleRealtime: EMPTY_MODULE_REALTIME_REGISTRY, moduleServices: Object.freeze({ database: Object.freeze({}) as unknown as ModuleDatabase, }) as ModuleRuntimeServices, moduleUi: createModuleUiPublicationRegistry(eligibility.eligibleModules), runtime, trustedOrigins: [trustedOrigin], }); return { app, modules, store }; } function owner(): InstallOptions { return { platformOwner: true, principal: createAuthenticatedPrincipal(viewerId) }; } interface ConfigurationBody { readonly moduleId: string; readonly version: string; readonly keys: readonly { readonly key: string; readonly type: string; readonly required: boolean; readonly description?: string; readonly default?: string | number | boolean; readonly values?: readonly string[]; readonly value?: string; readonly configured: boolean; }[]; readonly unknownKeys: readonly string[]; readonly restartRequired: boolean; } async function readConfiguration( app: Hono, moduleId = "announcements", ): Promise { const response = await app.request(`${MODULE_CONFIGURATION_PATH}/${moduleId}`, { headers: { origin: trustedOrigin }, }); expect(response.status).toBe(200); return (await response.json()) as ConfigurationBody; } function putConfiguration( app: Hono, values: unknown, moduleId = "announcements", origin = trustedOrigin, ) { return app.request(`${MODULE_CONFIGURATION_PATH}/${moduleId}`, { body: JSON.stringify({ values }), headers: { "content-type": "application/json", origin }, method: "PUT", }); } describe("GET /api/v1/module-configuration/:moduleId", () => { it("returns the module's declared schema with defaults and stored values", async () => { const { app } = await install({ ...owner(), records: [{ key: "bucket", moduleId: "announcements", value: "slides" }], secrets: { announcements: { storageCredential: "s3cret" } }, }); const body = await readConfiguration(app); expect(body.moduleId).toBe("announcements"); expect(body.keys.map((entry) => entry.key)).toEqual([ "slideSeconds", "pollIntervalSeconds", "bucket", "fit", "storageCredential", ]); expect(body.keys[0]).toEqual({ configured: false, default: 7, description: "How long each slide is shown.", key: "slideSeconds", required: false, type: "integer", }); expect(body.keys.find((entry) => entry.key === "bucket")).toMatchObject({ configured: true, required: true, value: "slides", }); expect(body.keys.find((entry) => entry.key === "fit")?.values).toEqual(["contain", "cover"]); }); it("never returns a secret value, only whether the deployment supplies one", async () => { const configured = await install({ ...owner(), secrets: { announcements: { storageCredential: "s3cret" } }, }); const body = await readConfiguration(configured.app); const secretEntry = body.keys.find((entry) => entry.key === "storageCredential"); expect(secretEntry).toEqual({ configured: true, key: "storageCredential", required: true, type: "secret", }); // Not redacted, not null, not a placeholder — the field is absent entirely, and the secret // string appears nowhere in the payload. expect(secretEntry).not.toHaveProperty("value"); expect(secretEntry).not.toHaveProperty("default"); expect(JSON.stringify(body)).not.toContain("s3cret"); }); it("reports an unsupplied secret as not configured, still without a value field", async () => { const { app } = await install(owner()); const body = await readConfiguration(app); expect(body.keys.find((entry) => entry.key === "storageCredential")).toEqual({ configured: false, key: "storageCredential", required: true, type: "secret", }); }); it("lists stored keys the current manifest no longer declares, without failing", async () => { const { app } = await install({ ...owner(), records: [ { key: "bucket", moduleId: "announcements", value: "slides" }, { key: "removedInThisVersion", moduleId: "announcements", value: "kept" }, ], }); const body = await readConfiguration(app); expect(body.unknownKeys).toEqual(["removedInThisVersion"]); expect(body.keys.map((entry) => entry.key)).not.toContain("removedInThisVersion"); }); it("returns a module declaring no configuration as an empty, valid document", async () => { const { app } = await install({ ...owner(), fixtures: [{ id: "calendar" }] }); const body = await readConfiguration(app, "calendar"); expect(body.keys).toEqual([]); expect(body.unknownKeys).toEqual([]); expect(body.restartRequired).toBe(false); }); it("refuses a module this process did not discover", async () => { const { app } = await install(owner()); const response = await app.request(`${MODULE_CONFIGURATION_PATH}/not-installed`, { headers: { origin: trustedOrigin }, }); expect(response.status).toBe(404); expect(await response.json()).toEqual({ code: "MODULE_NOT_INSTALLED" }); }); it("is not cached and varies on origin and cookie", async () => { const { app } = await install(owner()); const response = await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`, { headers: { origin: trustedOrigin }, }); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(response.headers.get("Vary")).toContain("Cookie"); }); it("reports a sanitized 503 when the store cannot be read, with no value or driver detail", async () => { const failing: ModuleConfigurationStore = { readSettings: async () => { throw new ModuleConfigurationStoreError("MODULE_CONFIGURATION_READ_FAILED"); }, replace: async () => { throw new ModuleConfigurationStoreError("MODULE_CONFIGURATION_WRITE_FAILED"); }, }; let bootRead = false; const { app } = await install({ ...owner(), store: { readSettings: async () => { if (!bootRead) { bootRead = true; return { records: [] }; } return failing.readSettings(); }, replace: (input) => failing.replace(input), }, }); const response = await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`, { headers: { origin: trustedOrigin }, }); expect(response.status).toBe(503); expect(await response.json()).toEqual({ code: "MODULE_CONFIGURATION_READ_FAILED" }); }); }); describe("PUT /api/v1/module-configuration/:moduleId", () => { it("stores values and reports that a restart is required", async () => { const { app, store } = await install({ ...owner(), records: [{ key: "bucket", moduleId: "announcements", value: "slides" }], }); const response = await putConfiguration(app, { bucket: "slides", slideSeconds: 12 }); expect(response.status).toBe(200); const body = (await response.json()) as ConfigurationBody; expect(body.keys.find((entry) => entry.key === "slideSeconds")).toMatchObject({ configured: true, value: "12", }); // The running process still holds the boot snapshot; claiming otherwise would be a lie. expect(body.restartRequired).toBe(true); expect((await store.readSettings()).records).toEqual([ { key: "bucket", moduleId: "announcements", value: "slides" }, { key: "slideSeconds", moduleId: "announcements", value: "12" }, ]); }); it("reports no pending restart when the submitted document matches what booted", async () => { const { app } = await install({ ...owner(), records: [{ key: "bucket", moduleId: "announcements", value: "slides" }], }); const response = await putConfiguration(app, { bucket: "slides" }); expect(((await response.json()) as ConfigurationBody).restartRequired).toBe(false); }); it("replaces the whole document, so an omitted key is cleared", async () => { const { app, store } = await install({ ...owner(), records: [ { key: "bucket", moduleId: "announcements", value: "slides" }, { key: "slideSeconds", moduleId: "announcements", value: "12" }, ], }); await putConfiguration(app, { bucket: "slides" }); expect((await store.readSettings()).records.map((record) => record.key)).toEqual(["bucket"]); }); it("never touches another module's stored values", async () => { const { app, store } = await install({ ...owner(), fixtures: [ { configuration: announcementsConfiguration, id: "announcements" }, { configuration: [{ key: "bucket", type: "string" }], id: "calendar" }, ], records: [ { key: "bucket", moduleId: "announcements", value: "slides" }, { key: "bucket", moduleId: "calendar", value: "events" }, ], }); await putConfiguration(app, { bucket: "renamed" }); expect((await store.readSettings()).records).toEqual([ { key: "bucket", moduleId: "announcements", value: "renamed" }, { key: "bucket", moduleId: "calendar", value: "events" }, ]); }); it("refuses to accept secret material over HTTP", async () => { const { app, store } = await install(owner()); const response = await putConfiguration(app, { bucket: "slides", storageCredential: "s3cret", }); expect(response.status).toBe(400); expect(await response.json()).toEqual({ code: "CONFIGURATION_SECRET_NOT_WRITABLE" }); // Nothing was written at all — not the secret, and not the valid key beside it. expect((await store.readSettings()).records).toEqual([]); }); it("rejects a key the module does not declare", async () => { const { app, store } = await install(owner()); const response = await putConfiguration(app, { bucket: "slides", mqttEndpoint: "mqtt://x" }); expect(response.status).toBe(400); expect(await response.json()).toEqual({ code: "CONFIGURATION_KEY_UNDECLARED" }); expect((await store.readSettings()).records).toEqual([]); }); it.each([ ["a string for an integer", { bucket: "slides", slideSeconds: "12" }], ["a float for an integer", { bucket: "slides", slideSeconds: 12.5 }], ["an integer below its declared minimum", { bucket: "slides", slideSeconds: 0 }], ["an integer above its declared maximum", { bucket: "slides", slideSeconds: 100000 }], ["a value outside a declared enum", { bucket: "slides", fit: "stretch" }], ["a string shorter than its declared minLength", { bucket: "" }], ["a nested object", { bucket: { name: "slides" } }], ["an array", { bucket: ["slides"] }], ["null", { bucket: null }], ])("rejects %s without storing anything", async (_label, values) => { const { app, store } = await install(owner()); const response = await putConfiguration(app, values); expect(response.status).toBe(400); expect((await store.readSettings()).records).toEqual([]); }); it.each([ ["a body with an unexpected top-level key", { values: {}, force: true }], ["a body with no values object", {}], ["a bare array body", []], ["a bare string body", "bucket=slides"], ])("rejects %s with a closed, structured validation failure", async (_label, body) => { const { app } = await install(owner()); const response = await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`, { body: JSON.stringify(body), headers: { "content-type": "application/json", origin: trustedOrigin }, method: "PUT", }); expect(response.status).toBe(400); expect(await response.json()).toEqual({ code: "INVALID_REQUEST" }); }); it.each([ "__proto__", "constructor", "prototype", ])("rejects the reserved property name %s as a submitted key", async (key) => { const { app, store } = await install(owner()); const response = await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`, { body: `{"values":{${JSON.stringify(key)}:"polluted"}}`, headers: { "content-type": "application/json", origin: trustedOrigin }, method: "PUT", }); expect(response.status).toBe(400); expect((await store.readSettings()).records).toEqual([]); // Nothing about the request reached `Object.prototype`. expect(({} as Record).polluted).toBeUndefined(); }); it("accepts an incomplete document, leaving the module blocked rather than refusing the save", async () => { const { app, store } = await install(owner()); // `bucket` is required and absent: an operator may save partial work. const response = await putConfiguration(app, { slideSeconds: 12 }); expect(response.status).toBe(200); expect((await store.readSettings()).records).toEqual([ { key: "slideSeconds", moduleId: "announcements", value: "12" }, ]); }); it("rejects an untrusted origin before authorization or the store runs", async () => { const { app, store } = await install(owner()); const response = await putConfiguration( app, { bucket: "slides" }, "announcements", untrustedOrigin, ); expect(response.status).toBe(403); expect(await response.json()).toEqual({ code: "UNTRUSTED_ORIGIN" }); expect((await store.readSettings()).records).toEqual([]); }); it("rejects a missing origin", async () => { const { app } = await install(owner()); const response = await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`, { body: JSON.stringify({ values: { bucket: "slides" } }), headers: { "content-type": "application/json" }, method: "PUT", }); expect(response.status).toBe(403); expect(await response.json()).toEqual({ code: "UNTRUSTED_ORIGIN" }); }); it("enforces the platform body limit with the one sanitized 413 contract", async () => { const { app } = await install(owner()); const oversized = JSON.stringify({ values: { bucket: "x".repeat(CORE_MUTATION_MAX_BODY_BYTES + 1024) }, }); const response = await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`, { body: oversized, headers: { "content-type": "application/json", origin: trustedOrigin }, method: "PUT", }); expect(response.status).toBe(413); expect(await response.json()).toEqual({ code: "REQUEST_BODY_TOO_LARGE" }); }); }); describe("module configuration authority", () => { it("refuses an anonymous caller on both verbs", async () => { const { app } = await install(); expect((await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`)).status).toBe(403); expect((await putConfiguration(app, { bucket: "slides" })).status).toBe(403); }); it("refuses an ordinary authenticated user who holds no configuration permission", async () => { const { app } = await install({ principal: createAuthenticatedPrincipal(viewerId) }); expect((await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`)).status).toBe(403); }); it("admits a non-owner holding exactly core.modules.configuration.manage", async () => { const { app } = await install({ grants: [String(CORE_MODULES_CONFIGURATION_MANAGE)], principal: createAuthenticatedPrincipal(viewerId), }); expect((await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`)).status).toBe(200); }); it("does not let the lifecycle permission configure a module, or the reverse", async () => { // The two authorities are deliberately separate: tuning how a module behaves and deciding // whether it runs at all are different decisions (ADR 0029). const lifecycleOnly = await install({ grants: [String(CORE_MODULES_LIFECYCLE_MANAGE)], principal: createAuthenticatedPrincipal(viewerId), }); expect( (await lifecycleOnly.app.request(`${MODULE_CONFIGURATION_PATH}/announcements`)).status, ).toBe(403); expect((await lifecycleOnly.app.request(MODULE_LIFECYCLE_PATH)).status).toBe(200); const configurationOnly = await install({ grants: [String(CORE_MODULES_CONFIGURATION_MANAGE)], principal: createAuthenticatedPrincipal(viewerId), }); expect((await configurationOnly.app.request(MODULE_LIFECYCLE_PATH)).status).toBe(403); expect( (await configurationOnly.app.request(`${MODULE_CONFIGURATION_PATH}/announcements`)).status, ).toBe(200); }); }); describe("configuration state is visible through the lifecycle resource", () => { it("reports a module blocked by missing required configuration, naming the key not the value", async () => { const { app } = await install({ ...owner(), secrets: { announcements: { storageCredential: "s3cret" } }, }); const response = await app.request(MODULE_LIFECYCLE_PATH, { headers: { origin: trustedOrigin }, }); const body = (await response.json()) as { readonly modules: readonly { readonly id: string; readonly runtime: { readonly state: string; readonly reason?: string; readonly configurationKey?: string; }; }[]; }; expect(body.modules[0]?.runtime).toEqual({ configurationKey: "bucket", reason: "configuration_missing", state: "blocked", }); }); it("reports a module blocked by a missing secret, without naming the secret's value", async () => { const { app } = await install({ ...owner(), records: [{ key: "bucket", moduleId: "announcements", value: "slides" }], }); const response = await app.request(MODULE_LIFECYCLE_PATH, { headers: { origin: trustedOrigin }, }); const raw = await response.text(); expect(raw).toContain("secret_missing"); expect(raw).toContain("storageCredential"); }); it("keeps a configuration-blocked module out of the mountable UI inventory", async () => { const { app } = await install({ ...owner(), fixtures: [ { configuration: [{ key: "bucket", type: "string", required: true }], id: "announcements" }, ], }); const inventory = (await (await app.request("/api/v1/module-ui")).json()) as { readonly modules: readonly { readonly id: string }[]; }; // Blocked by its own configuration behaves exactly like blocked by a disabled dependency: it is // not eligible, so it is not published. expect(inventory.modules).toEqual([]); }); }); describe("no secret or configuration value leaks through any surface", () => { it("keeps secret material out of every response body this resource produces", async () => { const { app } = await install({ ...owner(), records: [{ key: "bucket", moduleId: "announcements", value: "slides" }], secrets: { announcements: { storageCredential: "super-secret-value" } }, }); const listed = await ( await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`, { headers: { origin: trustedOrigin }, }) ).text(); const mutated = await (await putConfiguration(app, { bucket: "slides" })).text(); const lifecycle = await ( await app.request(MODULE_LIFECYCLE_PATH, { headers: { origin: trustedOrigin } }) ).text(); for (const payload of [listed, mutated, lifecycle]) { expect(payload).not.toContain("super-secret-value"); } }); it("does not serve a secret value even if a row for that key somehow exists in storage", async () => { // Unreachable through the API, which refuses to write a secret key, and unreachable through the // resolver, which never reads one. This plants the row directly to prove the *serialization* // boundary holds independently: a secret's `value` is omitted by construction, not filtered by a // branch someone could remove. const { app } = await install({ ...owner(), records: [ { key: "bucket", moduleId: "announcements", value: "slides" }, { key: "storageCredential", moduleId: "announcements", value: "planted-in-the-database" }, ], }); const body = await readConfiguration(app); const secretEntry = body.keys.find((entry) => entry.key === "storageCredential"); expect(secretEntry).not.toHaveProperty("value"); expect(secretEntry?.configured).toBe(false); expect(JSON.stringify(body)).not.toContain("planted-in-the-database"); }); it("never leaks a filesystem path, package name, or manifest path", async () => { const { app } = await install({ ...owner(), records: [{ key: "bucket", moduleId: "announcements", value: "slides" }], }); const listed = await ( await app.request(`${MODULE_CONFIGURATION_PATH}/announcements`, { headers: { origin: trustedOrigin }, }) ).text(); expect(listed).not.toContain("/tmp"); expect(listed).not.toContain("@test/module-"); expect(listed).not.toContain("ekklesia.module.json"); expect(listed).not.toContain("dist/"); }); }); describe("the configuration prefix is a sibling of the module route namespace", () => { it("does not sit under /api/v1/modules/, where an installed module could collide with it", async () => { // `config` and `configuration` are ordinary lowercase static segments under // `moduleRoutePathSchema`, so a module could legitimately register `PUT /config` and land on // `/api/v1/modules/announcements/config`. A sibling prefix cannot collide. expect(MODULE_CONFIGURATION_PATH.startsWith("/api/v1/modules/")).toBe(false); expect(MODULE_CONFIGURATION_PATH).toBe("/api/v1/module-configuration"); const { app } = await install(owner()); const collision = await app.request("/api/v1/modules/announcements/config", { body: JSON.stringify({ values: {} }), headers: { "content-type": "application/json", origin: trustedOrigin }, method: "PUT", }); expect(collision.status).toBe(404); expect(await collision.json()).toEqual({ code: "NOT_FOUND" }); }); });