feat: Module Package v1 carries the module's own license (ADR 0051)
CI / verify (pull_request) Successful in 58s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m53s
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 1m32s

Every Module Package v1 now declares the module's license and carries its
text. packageFormat stays 1; pre-ADR-0051 metadata fails closed.

- Contracts: ekklesia.package.json requires an SPDX `license` expression,
  validated with spdx-expression-parse, with UNLICENSED and non-expressions
  refused. The `files` list must hold exactly one non-empty root LICENSE.
  The package contracts move to the @ekklesia/contracts/module-package
  subpath, so the SPDX parser and its data (spdx-exceptions and
  spdx-license-ids ship no licence files) never reach a module bundle
  through the SDK's re-export of the contracts root.
- Packager: reads the source package.json's string SPDX declaration and
  never invents one. It requires an explicit --license-file (a non-empty
  regular file, no symlink, no parent search, no download) and copies its
  bytes unchanged to LICENSE, listed with size and SHA-256 and covered by
  the archive digest. THIRD_PARTY_NOTICES stays third-party only, and
  bundled Ekklesia code stays in `incorporates`.
- Verifier: rejects missing or invalid license metadata, and a missing,
  unlisted, duplicated, or corrupted LICENSE, through the schema and the
  existing strict inventory.
- Template: packaged as AGPL-3.0-or-later with the repository's canonical
  LICENSE, passed explicitly (Dockerfile, smoke, docs). Packing it twice
  gives byte-identical archives.

Started by Codex; reviewed and completed in Claude Code (empty license-text
refusal, schema non-empty LICENSE, symlink and empty-file tests).

ADR 0051 is Accepted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-07 18:51:03 -05:00
co-authored by Claude Opus 5.5
parent 30f61094e6
commit 85fae9e07e
22 changed files with 400 additions and 40 deletions
@@ -1,6 +1,6 @@
# 0051: Module packages declare their own license and carry its text
- Status: Proposed
- Status: Accepted
- Date: 2026-10-07
- Amends: [0049](0049-module-package-v1.md) — every Module Package v1 artifact gains an
authoritative package-level license declaration and a `LICENSE` payload. `packageFormat` stays 1.
+1 -1
View File
@@ -59,4 +59,4 @@ ADRs capture durable choices that affect more than one package or phase.
- [0048: Ekklesia ships one generic image, and installed modules live outside it](0048-generic-image-and-installed-module-storage.md) — Accepted
- [0049: A module release is one immutable, self-contained package: Ekklesia Module Package v1](0049-module-package-v1.md) — Accepted
- [0050: Module Database ABI v1: modules and Core exchange SQL, never ORM objects](0050-module-database-abi-v1.md) — Accepted
- [0051: Module packages declare their own license and carry its text](0051-module-package-license-material.md) — Proposed
- [0051: Module packages declare their own license and carry its text](0051-module-package-license-material.md) — Accepted
+4 -1
View File
@@ -679,7 +679,10 @@ Module Package v1, a deterministic `.tar.gz` with its SHA-256.
- **The packager.** `@ekklesia/module-pack` bundles the module's backend with every pure-JavaScript
dependency, the SDK, contracts, and Drizzle included. It copies the built browser artifacts and
migrations, generates the runtime `package.json`, `ekklesia.package.json` with every file's digest,
and `THIRD_PARTY_NOTICES`, and writes the archive.
copies the explicitly supplied module `LICENSE` bytes, generates `THIRD_PARTY_NOTICES` for bundled
third parties, and writes the archive. The source module's string SPDX expression is required and
becomes the package metadata's authoritative `license`; the packager neither infers nor downloads
license material.
- **The verifier.** Core's `verifyModulePackage` reads the archive in memory and refuses links,
special files, and escaping paths. It checks every digest and identity, the target and runtime,
runs the platform's own module, schema, and migration validation, and loads the backend once in a
+15 -9
View File
@@ -598,26 +598,31 @@ CI / migration-smoke (pull_request)
Ekklesia Module Package v1 (ADR 0049) is proven at three levels, all in `pnpm verify`:
- `packages/contracts/tests/module-package.test.ts`: the `ekklesia.package.json` schema, target
names, and the generated runtime `package.json`.
- `packages/contracts/tests/module-package.test.ts`: the `ekklesia.package.json` schema, required
SPDX expression and `LICENSE` inventory entry, target names, and the generated runtime
`package.json`.
- `core/tests/module-package-verification.test.ts`: the verifier against hand-built archives,
including what the packager never produces. A valid package verifies and extracts with normalized
modes. A wrong digest, a corrupted or missing or unlisted file, disagreeing identities, an unknown
format, unknown transitional metadata, an incompatible target, Node.js range, SDK, or legacy
database runtime, ABI metadata carrying a forbidden transitional runtime, malformed ABI schema
metadata, a missing entrypoint, a backend that imports something the package lacks, absolute and
metadata, missing or malformed license metadata, missing, unlisted, duplicated, or corrupted
`LICENSE`, a missing entrypoint, a backend that imports something the package lacks, absolute and
`..` paths, symbolic and hard links, device nodes and FIFOs, setuid and setgid entries, a second
top-level directory, and a duplicate entry each fail with their own sanitized code. The backend
load check runs in a real child process and selects the schema contract from the manifest.
- `packages/module-pack/tests/`: the packager against generated fixture modules (payload contents,
generated `package.json`, metadata, bundled backend with only built-in imports, refusal of native
and platform-specific dependencies and of unknown or textless licences, notices, byte-identical
archives, normalized tar and gzip headers), and end to end against the built module template:
and platform-specific dependencies and of unknown or textless third-party licences, explicit
module SPDX and license-file validation, unchanged `LICENSE` bytes and inventory digest, notices,
byte-identical archives, normalized tar and gzip headers), and end to end against the built module template:
packed twice to identical bytes without changing the workspace's install state, verified by Core's
verifier with a deliberately different Core Drizzle version, extracted elsewhere, and loaded with
Node's own resolution hooks proving nothing outside the package is resolved. The package contains
module-side Drizzle but no `transitional.databaseRuntime`. Turbo builds the template and Core
first, and runs the template's own tests (which rebuild its UI in place) before these.
the repository's exact AGPL `LICENSE`, authoritative `AGPL-3.0-or-later` metadata, module-side
Drizzle but no `transitional.databaseRuntime`, and no tooling-only SPDX parser dependency. Turbo
builds the template and Core first, and runs the template's own tests (which rebuild its UI in
place) before these.
## The generic platform image and the reference deployment
@@ -636,8 +641,9 @@ These need Docker and build images locally; nothing is pushed or published.
- `pnpm deploy:smoke` (`scripts/smoke-deploy.sh`) runs the reference deployment end to end on the
generic image with empty module storage. With `EKKLESIA_SMOKE_TOPOLOGY=split`, the same image also
runs the installed-module lifecycle:
- the module template is packaged by the canonical packager (Module Package v1, ADR 0049),
as ABI v1, verified by the image's own verifier, and installed into module storage;
- the module template is packaged by the canonical packager (Module Package v1, ADR 0049 and
ADR 0051), with its explicit AGPL declaration and license bytes, as ABI v1, verified by the
image's own verifier, and installed into module storage;
- its migration is planned and explicitly applied, and the API restarted;
- a real browser mounts it;
- it is removed and the API restarted, and Core still runs, migrations still plan and apply, and
+22 -10
View File
@@ -97,6 +97,7 @@ development dependency like `@ekklesia/module-ui-build`:
```bash
pnpm exec ekklesia-pack-module --module . --target any --out ../packages \
--license-file ./LICENSE \
--source-revision "$(git rev-parse HEAD)"
```
@@ -110,7 +111,8 @@ The archive holds one directory named with your module id:
<id>/
├── package.json generated: name, version, private, "type": "module", the layer marker
├── ekklesia.module.json your manifest, byte for byte
├── ekklesia.package.json package metadata: target, runtime, provenance, file digests
├── ekklesia.package.json package metadata: license, target, runtime, provenance, file digests
├── LICENSE your explicitly supplied module license material, byte for byte
├── THIRD_PARTY_NOTICES licence and notice text of every third-party package bundled
├── dist/server/index.js your backend entrypoints, bundled
├── dist/schema/index.js
@@ -130,11 +132,18 @@ The archive holds one directory named with your module id:
- **`any` is the only target today.** A dependency that is native or platform-specific (a compiled
addon, a package that declares an operating system or CPU, or one that loads a per-platform binary
such as `sharp`) cannot be in an `any` package, and packaging fails. Native targets come later.
- **Licences.** `THIRD_PARTY_NOTICES` credits every third-party package your bundles contain, with
its declared licence and the licence and notice files it ships. A package that declares no licence
fails packaging, and so does one that ships no licence text, unless its licence's standard text is
complete without a copyright line (Apache-2.0, which the packager then supplies and says so). The
Ekklesia platform's own packages are listed in `ekklesia.package.json` under `incorporates`.
- **Your module's licence.** Your source `package.json` must contain a non-empty string SPDX
expression. The packager validates its syntax, rejects `UNLICENSED`, and copies it unchanged to
the required `ekklesia.package.json.license`. `--license-file` is also required: it names the
regular file whose exact bytes become the root `LICENSE` payload and inventory entry. The
packager never searches parent directories, guesses text from the expression, or downloads it.
- **Third-party licences.** `THIRD_PARTY_NOTICES` separately credits every third-party package your
bundles contain, with its declared licence and the licence and notice files it ships. A dependency
that declares no licence fails packaging, and so does one that ships no licence text, unless its
licence's standard text is complete without a copyright line (Apache-2.0, which the packager then
supplies and says so). The module itself is not added to this notice file. Bundled Ekklesia
packages remain listed in `ekklesia.package.json` under `incorporates`; one applicable AGPL text
is sufficient for current official modules.
- **Deterministic.** The same packager version and the same built inputs produce the same archive,
byte for byte, wherever the checkout is.
@@ -167,10 +176,13 @@ later version, `AGPL-3.0-or-later` ([LICENSE](../../LICENSE)), and official Ekkl
same license. Declare it in your module's development `package.json`
(`"license": "AGPL-3.0-or-later"`), as the module template does.
A Module Package v1 does not yet record a module's own license: its runtime `package.json` and
`ekklesia.package.json` have fixed fields, and its contents are fixed, by ADR 0049. Carrying the
module's license, and the license text for the Ekklesia code bundled into its backend, needs a
change to that format.
A Module Package v1 records that validated expression in its required
`ekklesia.package.json.license` and contains the exact explicitly supplied bytes at `LICENSE`
([ADR 0051](../architecture/decisions/0051-module-package-license-material.md)). The generated
runtime `package.json` is not authoritative for licensing. `THIRD_PARTY_NOTICES` remains only for
redistributed third-party dependencies, while bundled Ekklesia components remain represented by
`incorporates`. This strict-reader addition keeps `packageFormat` at `1`; older pre-0051 artifacts
without the field and payload fail closed.
### Removing a module keeps its data