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
View File
@@ -9,7 +9,6 @@ export * from "./module-http.js";
export * from "./module-lifecycle.js";
export * from "./module-manifest.js";
export * from "./module-migration.js";
export * from "./module-package.js";
export * from "./module-realtime.js";
export * from "./module-runtime.js";
export * from "./module-schema.js";
+36
View File
@@ -1,4 +1,5 @@
import { z } from "zod";
import parseSpdxExpression from "spdx-expression-parse";
import { moduleIdSchema } from "./identifiers.js";
import { semanticVersionRangeSchema, semanticVersionSchema } from "./versions.js";
@@ -11,11 +12,30 @@ import { semanticVersionRangeSchema, semanticVersionSchema } from "./versions.js
*/
export const MODULE_PACKAGE_FORMAT = 1;
export const MODULE_PACKAGE_LICENSE_FILENAME = "LICENSE";
export const MODULE_PACKAGE_METADATA_FILENAME = "ekklesia.package.json";
export const MODULE_PACKAGE_NOTICES_FILENAME = "THIRD_PARTY_NOTICES";
/** The portable target: a package with no native or platform-specific file. */
export const MODULE_PACKAGE_ANY_TARGET = "any";
/** The module publisher's package-level license declaration (ADR 0051). This validates SPDX syntax,
* not whether Ekklesia approves, recommends, or considers the license legally compatible. */
export const modulePackageLicenseExpressionSchema = z
.string()
.min(1)
.max(1024)
.superRefine((value, context) => {
if (value === "UNLICENSED") {
context.addIssue({ code: "custom", message: "Must be an SPDX license expression." });
return;
}
try {
parseSpdxExpression(value);
} catch {
context.addIssue({ code: "custom", message: "Must be an SPDX license expression." });
}
});
/**
* `any`, or `<platform>-<arch>[-<libc>]` in Node.js's own `process.platform` and `process.arch`
* vocabulary. On Linux the C library is required, because a native addon built against one does
@@ -104,6 +124,8 @@ export const modulePackageMetadataSchema = z
.strictObject({
packageFormat: z.literal(MODULE_PACKAGE_FORMAT),
module: z.strictObject({ id: moduleIdSchema, version: semanticVersionSchema }),
/** The module's authoritative package-release license declaration (ADR 0051). */
license: modulePackageLicenseExpressionSchema,
target: modulePackageTargetSchema,
runtime: modulePackageRuntimeSchema,
provenance: z.strictObject({
@@ -124,6 +146,16 @@ export const modulePackageMetadataSchema = z
files: z.array(modulePackageFileSchema),
})
.superRefine((metadata, context) => {
const licenseFiles = metadata.files.filter(
(file) => file.path === MODULE_PACKAGE_LICENSE_FILENAME,
);
if (licenseFiles.length !== 1 || licenseFiles[0]?.size === 0) {
context.addIssue({
code: "custom",
message: "The payload must contain exactly one non-empty root LICENSE file.",
path: ["files"],
});
}
if (
metadata.target === MODULE_PACKAGE_ANY_TARGET &&
(metadata.runtime.nodeApi !== undefined ||
@@ -157,6 +189,10 @@ export function safeParseModulePackageMetadata(input: unknown) {
return modulePackageMetadataSchema.safeParse(input);
}
export function safeParseModulePackageLicenseExpression(input: unknown) {
return modulePackageLicenseExpressionSchema.safeParse(input);
}
export function safeParseModulePackageTarget(input: unknown) {
return modulePackageTargetSchema.safeParse(input);
}