feat: standalone module-build readiness for the public @ekklesia packages (ADR 0053)
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 28s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m42s

Makes the public module-development package set usable from a module
repository outside the monorepo. Nothing is published.

- Curated @ekklesia/module-sdk: an explicit author API, no contracts
  barrel, no identity/authorization internals, no Drizzle and no legacy
  ModuleDatabase type. defineModuleRuntime returns the SDK's own named
  interfaces so a module's emitted declarations never reach into
  @ekklesia/contracts. defineModuleSchema is gone; createModuleTable and
  defineModuleSchemaMetadata live in @ekklesia/module-database.
- Drizzle: no longer a dependency of module-sdk or contracts. Core keeps
  the legacy Drizzle schema validator (moved to core/src/modules) and
  its legacy runtime support; drizzle-orm is an optional peer only.
- React is a peer (plus devDependency) of @ekklesia/ui; the boundary
  checker allows exactly that consumer-selected peer.
- @ekklesia/typescript-config is publishable; module-ui-host is private.
  Every public package carries "ekklesia.distribution":
  "module-development" and no monorepo-root prepack.
- module-pack reads the Node.js range from the module's own engines.node
  and recognizes Ekklesia packages by name plus that marker, so
  registry-installed packages are incorporated, SDK-compatibility
  checked, and never credited as third-party.
- scripts/pack-public-packages.ts (pnpm packages:pack) rebuilds, packs
  and inspects the nine public tarballs; scripts/test-standalone-module.sh
  (pnpm test:standalone-module, new CI job) installs them into a fixture
  module outside the repository and proves it typechecks, builds,
  packages, and verifies.
- Public-package boundary tests and module-development docs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-08 07:28:54 -05:00
co-authored by Claude Opus 5.5
parent 9426c83109
commit 89d5d0f2da
63 changed files with 2320 additions and 237 deletions
@@ -1,18 +1,69 @@
import {
type LegacyModuleRuntimeDefinition,
type Awaitable,
type CapabilityRegistrar,
type CapabilityResolver,
type ModuleRegistrationContext as ContractModuleRegistrationContext,
type ModuleRuntimeContext as ContractModuleRuntimeContext,
type ModuleRuntimeServices as ContractModuleRuntimeServices,
type ModuleConfiguration,
type ModuleDatabaseAbiRuntimeDefinition,
type ModuleRuntimeDefinition,
type ModuleDatabaseExecutor,
type ModuleId,
parseModuleRuntimeDefinition,
} from "@ekklesia/contracts";
export function defineModuleRuntime<const Definition extends ModuleDatabaseAbiRuntimeDefinition>(
definition: Definition,
): Readonly<Definition>;
export function defineModuleRuntime<const Definition extends LegacyModuleRuntimeDefinition>(
definition: Definition,
): Readonly<Definition>;
export function defineModuleRuntime<const Definition extends ModuleRuntimeDefinition>(
definition: Definition,
): Readonly<Definition> {
return parseModuleRuntimeDefinition(definition) as Readonly<Definition>;
/**
* What a module's `register` hook receives. `Database` is what `services.database` is for the
* module: the Module Database ABI v1 executor when it declares `database: { abi: 1 }`, and `never`
* when it declares no database, so code cannot reach a database it never declared.
*/
export type ModuleRegistrationContext<
Database extends ModuleDatabaseExecutor = ModuleDatabaseExecutor,
> = ContractModuleRegistrationContext<Database>;
/** What a module's `start` and `stop` hooks receive. */
export type ModuleRuntimeContext<Database extends ModuleDatabaseExecutor = ModuleDatabaseExecutor> =
ContractModuleRuntimeContext<Database>;
/** The platform services every hook and handler receives. `database` is the ABI v1 executor for a
* module that declares one, and `never` otherwise. */
export type ModuleRuntimeServices<
Database extends ModuleDatabaseExecutor = ModuleDatabaseExecutor,
> = ContractModuleRuntimeServices<Database>;
/** A module that declares the Module Database ABI v1 (ADR 0050). Its hooks and route handlers see
* `services.database` as the ABI v1 executor. */
export interface ModuleRuntimeDefinitionWithDatabase extends ModuleDatabaseAbiRuntimeDefinition {}
/** A module that uses no database. Its hooks see `services.database` as `never`. */
export interface ModuleRuntimeDefinitionWithoutDatabase {
readonly id: ModuleId;
readonly version: string;
readonly database?: never;
readonly register?: (context: ModuleRegistrationContext<never>) => Awaitable<void>;
readonly start?: (context: ModuleRuntimeContext<never>) => Awaitable<void>;
readonly stop?: (context: ModuleRuntimeContext<never>) => Awaitable<void>;
}
/** Every runtime definition the Module SDK supports. The legacy Drizzle database facade is not one
* of them: a module that uses a database declares `database: { abi: 1 }` (ADR 0053). */
export type ModuleRuntimeDefinition =
| ModuleRuntimeDefinitionWithDatabase
| ModuleRuntimeDefinitionWithoutDatabase;
/**
* Validates and freezes a module's server runtime. The result is typed as one of this package's own
* named interfaces, so a module that emits declarations can always name its runtime's type through
* `@ekklesia/module-sdk`, without depending on the packages behind it.
*/
export function defineModuleRuntime(
definition: ModuleRuntimeDefinitionWithDatabase,
): ModuleRuntimeDefinitionWithDatabase;
export function defineModuleRuntime(
definition: ModuleRuntimeDefinitionWithoutDatabase,
): ModuleRuntimeDefinitionWithoutDatabase;
export function defineModuleRuntime(definition: ModuleRuntimeDefinition): ModuleRuntimeDefinition {
return parseModuleRuntimeDefinition(definition) as ModuleRuntimeDefinition;
}
export type { Awaitable, CapabilityRegistrar, CapabilityResolver, ModuleConfiguration };
@@ -1,19 +0,0 @@
import {
type ModuleId,
type ModuleSchemaDefinition,
moduleTableName,
moduleTablePrefix,
parseModuleSchemaDefinition,
} from "@ekklesia/contracts";
import { pgTableCreator } from "drizzle-orm/pg-core";
export function createModuleTable(moduleId: ModuleId) {
moduleTablePrefix(moduleId);
return pgTableCreator((localName) => moduleTableName(moduleId, localName));
}
export function defineModuleSchema<const Definition extends ModuleSchemaDefinition>(
definition: Definition,
): Readonly<Definition> {
return parseModuleSchemaDefinition(definition) as Readonly<Definition>;
}
+24
View File
@@ -0,0 +1,24 @@
import type {
ModuleHttpRouteRegistrar as ContractModuleHttpRouteRegistrar,
ModuleRouteDefinitionInput as ContractModuleRouteDefinitionInput,
ModuleRouteHandler as ContractModuleRouteHandler,
ModuleRouteHandlerContext as ContractModuleRouteHandlerContext,
ModuleDatabaseExecutor,
} from "@ekklesia/contracts";
/** What a route handler receives. `services.database` is the module's ABI v1 executor, or `never`
* for a module that declares no database. */
export type ModuleRouteHandlerContext<
Database extends ModuleDatabaseExecutor = ModuleDatabaseExecutor,
> = ContractModuleRouteHandlerContext<Database>;
export type ModuleRouteHandler<Database extends ModuleDatabaseExecutor = ModuleDatabaseExecutor> =
ContractModuleRouteHandler<Database>;
export type ModuleRouteDefinitionInput<
Database extends ModuleDatabaseExecutor = ModuleDatabaseExecutor,
> = ContractModuleRouteDefinitionInput<Database>;
export type ModuleHttpRouteRegistrar<
Database extends ModuleDatabaseExecutor = ModuleDatabaseExecutor,
> = ContractModuleHttpRouteRegistrar<Database>;
+102 -3
View File
@@ -1,3 +1,102 @@
export * from "@ekklesia/contracts";
export * from "./define-module-runtime.js";
export * from "./define-module-schema.js";
/**
* `@ekklesia/module-sdk`: the supported module-author API (ADR 0053).
*
* Every export is chosen. The platform's internal contracts (identity parsing, authorization,
* lifecycle, manifest internals beyond what a module validates about itself) are not re-exported,
* and neither is the legacy Drizzle database facade. A module that uses a database declares
* `database: { abi: 1 }` and gets its database types and helpers from `@ekklesia/module-database`.
* Nothing here loads or requires Drizzle.
*/
export type {
Awaitable,
CapabilityId,
CapabilityRegistrar,
CapabilityResolver,
ModuleId,
RegisteredCapability,
} from "@ekklesia/contracts";
// HTTP routes (ADR 0015, ADR 0042).
// Who is calling (ADR 0013, ADR 0036): only what a route handler receives and branches on.
// Configuration (ADR 0029).
// Object storage (ADR 0030, ADR 0031).
// Realtime (ADR 0032).
// The module's own static artifacts, for a module's tests to validate them the way Core does.
export {
ANONYMOUS_PRINCIPAL,
type EkklesiaAnonymousPrincipal,
type EkklesiaAuthenticatedPrincipal,
type EkklesiaDeviceId,
type EkklesiaDevicePrincipal,
type EkklesiaPrincipal,
type EkklesiaUserId,
EMPTY_MODULE_CONFIGURATION,
EMPTY_MODULE_STORAGE,
isAnonymousPrincipal,
isAuthenticatedPrincipal,
isDevicePrincipal,
isModuleHttpMethod,
isModuleRouteBodyMode,
isValidModuleStorageKey,
isValidModuleStorageReadUrlFilename,
MODULE_HTTP_BODY_MODES,
MODULE_HTTP_METHODS,
MODULE_REALTIME_CHANNEL_MAX_LENGTH,
MODULE_REALTIME_ERROR_CODES,
MODULE_REALTIME_MAX_CHANNELS_PER_MODULE,
MODULE_REALTIME_MAX_PAYLOAD_BYTES,
MODULE_REALTIME_MAX_SUBSCRIBERS_PER_CHANNEL,
MODULE_ROUTE_ABSOLUTE_MAX_BODY_BYTES,
MODULE_ROUTE_MAX_BODY_BYTES_CEILING,
MODULE_ROUTE_STREAMED_MAX_BODY_BYTES_CEILING,
MODULE_STORAGE_ERROR_CODES,
MODULE_STORAGE_KEY_MAX_BYTES,
MODULE_STORAGE_READ_URL_DEFAULT_SECONDS,
MODULE_STORAGE_READ_URL_FILENAME_MAX_LENGTH,
MODULE_STORAGE_READ_URL_MAX_SECONDS,
MODULE_STORAGE_READ_URL_MIN_SECONDS,
type ModuleConfiguration,
type ModuleConfigurationValue,
type ModuleHttpMethod,
type ModuleManifest,
type ModuleRealtimeChannel,
type ModuleRealtimeChannelDefinitionInput,
type ModuleRealtimeChannelRegistrar,
ModuleRealtimeError,
type ModuleRealtimeErrorCode,
type ModuleRealtimeJsonPrimitive,
type ModuleRealtimeJsonValue,
type ModuleRealtimePublisher,
type ModuleRouteBodyMode,
type ModuleRoutePath,
type ModuleStorage,
type ModuleStorageBody,
type ModuleStorageBucket,
ModuleStorageError,
type ModuleStorageErrorCode,
type ModuleStorageListing,
type ModuleStorageListOptions,
type ModuleStorageObject,
type ModuleStorageObjectMetadata,
type ModuleStoragePutOptions,
type ModuleStorageReadUrl,
type ModuleStorageReadUrlOptions,
parseDrizzleMigrationJournal,
parseModuleManifest,
parseModuleMigrationPolicy,
} from "@ekklesia/contracts";
export {
defineModuleRuntime,
type ModuleRegistrationContext,
type ModuleRuntimeContext,
type ModuleRuntimeDefinition,
type ModuleRuntimeDefinitionWithDatabase,
type ModuleRuntimeDefinitionWithoutDatabase,
type ModuleRuntimeServices,
} from "./define-module-runtime.js";
export type {
ModuleHttpRouteRegistrar,
ModuleRouteDefinitionInput,
ModuleRouteHandler,
ModuleRouteHandlerContext,
} from "./http.js";