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
+6
View File
@@ -10,6 +10,10 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./module-package": {
"types": "./dist/module-package.d.ts",
"import": "./dist/module-package.js"
}
},
"scripts": {
@@ -22,12 +26,14 @@
"dependencies": {
"drizzle-orm": "0.45.2",
"semver": "7.8.5",
"spdx-expression-parse": "5.0.0",
"zod": "4.4.3"
},
"devDependencies": {
"@biomejs/biome": "2.5.3",
"@types/node": "24.13.3",
"@types/semver": "7.7.1",
"@types/spdx-expression-parse": "4.0.0",
"typescript": "7.0.2",
"vitest": "4.1.10"
},
-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);
}
@@ -1,13 +1,15 @@
import { describe, expect, it } from "vitest";
import {
MODULE_PACKAGE_FORMAT,
MODULE_PACKAGE_LICENSE_FILENAME,
MODULE_PACKAGE_METADATA_FILENAME,
modulePackageArchiveFileName,
modulePackageRuntimePackage,
safeParseModulePackageLicenseExpression,
safeParseModulePackageMetadata,
safeParseModulePackageRuntimePackage,
safeParseModulePackageTarget,
} from "../src/index.js";
} from "../src/module-package.js";
const digest = "a".repeat(64);
@@ -15,6 +17,7 @@ function metadata(overrides: Record<string, unknown> = {}): Record<string, unkno
return {
packageFormat: 1,
module: { id: "example", version: "0.1.0" },
license: "AGPL-3.0-or-later",
target: "any",
runtime: { node: ">=24.11.0 <25" },
provenance: {
@@ -24,6 +27,7 @@ function metadata(overrides: Record<string, unknown> = {}): Record<string, unkno
incorporates: { "@ekklesia/contracts": "0.1.2", "@ekklesia/module-sdk": "0.1.2" },
transitional: { databaseRuntime: { "drizzle-orm": "0.45.2" } },
files: [
{ path: "LICENSE", size: 9, sha256: digest },
{ path: "THIRD_PARTY_NOTICES", size: 10, sha256: digest },
{ path: "dist/server/index.js", size: 20, sha256: digest },
{ path: "ekklesia.module.json", size: 30, sha256: digest },
@@ -36,12 +40,46 @@ function metadata(overrides: Record<string, unknown> = {}): Record<string, unkno
describe("Ekklesia Module Package v1 metadata (ADR 0049)", () => {
it("names the format, its metadata file, and the recommended archive file name", () => {
expect(MODULE_PACKAGE_FORMAT).toBe(1);
expect(MODULE_PACKAGE_LICENSE_FILENAME).toBe("LICENSE");
expect(MODULE_PACKAGE_METADATA_FILENAME).toBe("ekklesia.package.json");
expect(modulePackageArchiveFileName("example", "0.1.0", "any")).toBe(
"example-0.1.0-any.tar.gz",
);
});
it("accepts SPDX license expressions and rejects non-expressions without a policy allowlist", () => {
for (const expression of [
"AGPL-3.0-or-later",
"MIT OR Apache-2.0",
"(MIT OR Apache-2.0) AND BSD-3-Clause",
]) {
expect(safeParseModulePackageLicenseExpression(expression).success, expression).toBe(true);
}
for (const expression of ["", "UNLICENSED", "MIT OR", "not a license", {}]) {
expect(safeParseModulePackageLicenseExpression(expression).success, String(expression)).toBe(
false,
);
}
});
it("requires the package license declaration and canonical LICENSE payload entry", () => {
const withoutLicense = metadata();
delete withoutLicense.license;
expect(safeParseModulePackageMetadata(withoutLicense).success).toBe(false);
const withoutLicenseFile = metadata();
withoutLicenseFile.files = (withoutLicenseFile.files as { path: string }[]).filter(
(file) => file.path !== "LICENSE",
);
expect(safeParseModulePackageMetadata(withoutLicenseFile).success).toBe(false);
const emptyLicenseFile = metadata();
emptyLicenseFile.files = (emptyLicenseFile.files as { path: string; size: number }[]).map(
(file) => (file.path === "LICENSE" ? { ...file, size: 0 } : file),
);
expect(safeParseModulePackageMetadata(emptyLicenseFile).success).toBe(false);
});
it("accepts a complete any-target package description", () => {
expect(safeParseModulePackageMetadata(metadata()).success).toBe(true);
});
@@ -97,7 +135,9 @@ describe("Ekklesia Module Package v1 metadata (ADR 0049)", () => {
).toBe(false);
// Byte-wise, not locale order: upper case sorts before lower case.
expect(
safeParseModulePackageMetadata(metadata({ files: [entry("THIRD"), entry("dist")] })).success,
safeParseModulePackageMetadata(
metadata({ files: [entry("LICENSE"), entry("THIRD"), entry("dist")] }),
).success,
).toBe(true);
});
+11 -3
View File
@@ -9,7 +9,7 @@ import { ModulePackError, type PackModuleOptions, packModule } from "./pack-modu
* `ekklesia-pack-module` — the canonical Ekklesia module packager (ADR 0049).
*
* ekklesia-pack-module --module <built module directory> --target any --out <directory>
* --source-revision <full commit id> [--repository <url>]
* --license-file <file> --source-revision <full commit id> [--repository <url>]
*
* Writes `<id>-<version>-any.tar.gz` and its `.sha256` line to the output directory, and nothing
* else anywhere. Run it in the module's CI after building the module.
@@ -18,9 +18,10 @@ import { ModulePackError, type PackModuleOptions, packModule } from "./pack-modu
class PackArgumentsError extends Error {}
const USAGE =
"Usage: ekklesia-pack-module --module <directory> --target any --out <directory> --source-revision <commit> [--repository <url>]";
"Usage: ekklesia-pack-module --module <directory> --target any --out <directory> --license-file <file> --source-revision <commit> [--repository <url>]";
const OPTIONS: Readonly<Record<string, keyof PackModuleOptions>> = Object.freeze({
"--license-file": "licenseFile",
"--module": "moduleDirectory",
"--out": "outDirectory",
"--repository": "repository",
@@ -43,12 +44,19 @@ export function parsePackModuleArguments(argv: readonly string[]): PackModuleOpt
}
values.set(option, value);
}
const required = ["moduleDirectory", "outDirectory", "sourceRevision", "target"] as const;
const required = [
"licenseFile",
"moduleDirectory",
"outDirectory",
"sourceRevision",
"target",
] as const;
if (required.some((option) => !values.has(option))) {
throw new PackArgumentsError(USAGE);
}
const repository = values.get("repository");
return {
licenseFile: values.get("licenseFile") ?? "",
moduleDirectory: values.get("moduleDirectory") ?? "",
outDirectory: values.get("outDirectory") ?? "",
...(repository === undefined ? {} : { repository }),
+36 -3
View File
@@ -3,17 +3,18 @@ import { lstat, mkdir, readdir, readFile, realpath, rename, writeFile } from "no
import { isBuiltin } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { type ModuleManifest, safeParseModuleManifest } from "@ekklesia/contracts";
import {
MODULE_PACKAGE_ANY_TARGET,
MODULE_PACKAGE_FORMAT,
MODULE_PACKAGE_LICENSE_FILENAME,
MODULE_PACKAGE_METADATA_FILENAME,
MODULE_PACKAGE_NOTICES_FILENAME,
type ModuleManifest,
modulePackageArchiveFileName,
modulePackageRuntimePackage,
safeParseModuleManifest,
safeParseModulePackageLicenseExpression,
safeParseModulePackageMetadata,
} from "@ekklesia/contracts";
} from "@ekklesia/contracts/module-package";
import { analyzeModuleUi, ModuleUiBuildError } from "@ekklesia/module-ui-build";
import { build, type Message, type Plugin } from "esbuild";
import { minVersion, satisfies, validRange } from "semver";
@@ -40,6 +41,8 @@ export type ModulePackErrorCode =
| "MODULE_PACK_IDENTITY_MISMATCH"
| "MODULE_PACK_INCORPORATED_CONFLICT"
| "MODULE_PACK_LAYOUT_CONFLICT"
| "MODULE_PACK_MODULE_LICENSE_FILE_INVALID"
| "MODULE_PACK_MODULE_LICENSE_INVALID"
| "MODULE_PACK_LICENCE_TEXT_MISSING"
| "MODULE_PACK_LICENCE_UNKNOWN"
| "MODULE_PACK_MANIFEST_INVALID"
@@ -71,6 +74,10 @@ const messages: Readonly<Record<ModulePackErrorCode, string>> = Object.freeze({
MODULE_PACK_INCORPORATED_CONFLICT:
"The package bundles more than one version of an Ekklesia package.",
MODULE_PACK_LAYOUT_CONFLICT: "Two package files would have the same path.",
MODULE_PACK_MODULE_LICENSE_FILE_INVALID:
"The module license text must be supplied as an explicit, non-empty, readable regular file.",
MODULE_PACK_MODULE_LICENSE_INVALID:
"The source package.json must declare a valid SPDX license expression as a string.",
MODULE_PACK_LICENCE_TEXT_MISSING:
"A bundled package ships no licence text, and its licence has no complete standard text to supply.",
MODULE_PACK_LICENCE_UNKNOWN: "A bundled package declares no licence that permits redistribution.",
@@ -115,6 +122,8 @@ export class ModulePackError extends Error {
export interface PackModuleOptions {
/** A built module directory: its manifest, package.json, and compiled entrypoints. */
readonly moduleDirectory: string;
/** Explicit license-text input. Its bytes are copied unchanged to the package root as LICENSE. */
readonly licenseFile: string;
/** `any` only, in this release. */
readonly target: string;
readonly outDirectory: string;
@@ -377,6 +386,28 @@ export async function packModule(options: PackModuleOptions): Promise<ModulePack
) {
throw new ModulePackError("MODULE_PACK_IDENTITY_MISMATCH");
}
const parsedLicense = safeParseModulePackageLicenseExpression(developmentPackage.license);
if (!parsedLicense.success) {
throw new ModulePackError("MODULE_PACK_MODULE_LICENSE_INVALID");
}
const license = parsedLicense.data;
let licenseBytes: Buffer;
try {
if (
typeof options.licenseFile !== "string" ||
options.licenseFile === "" ||
!(await lstat(path.resolve(options.licenseFile))).isFile()
) {
throw new Error("not a regular file");
}
licenseBytes = await readFile(path.resolve(options.licenseFile));
if (licenseBytes.length === 0) {
// An empty file would satisfy the inventory while carrying no license material at all.
throw new Error("empty");
}
} catch {
throw new ModulePackError("MODULE_PACK_MODULE_LICENSE_FILE_INVALID");
}
const nodeRange = options.nodeRange ?? (await workspaceNodeRange(moduleDirectory));
if (validRange(nodeRange) === null) {
@@ -493,6 +524,7 @@ export async function packModule(options: PackModuleOptions): Promise<ModulePack
}
}
add("ekklesia.module.json", manifestBytes);
add(MODULE_PACKAGE_LICENSE_FILENAME, licenseBytes);
add(
"package.json",
Buffer.from(
@@ -522,6 +554,7 @@ export async function packModule(options: PackModuleOptions): Promise<ModulePack
const metadata = {
packageFormat: MODULE_PACKAGE_FORMAT,
module: { id: manifest.id, version: manifest.version },
license,
target: MODULE_PACKAGE_ANY_TARGET,
runtime: { node: nodeRange },
provenance: {
+9
View File
@@ -13,11 +13,14 @@ describe("ekklesia-pack-module arguments", () => {
"any",
"--out",
"dist/packages",
"--license-file",
"LICENSE",
"--source-revision",
revision,
]),
).toEqual({
moduleDirectory: "templates/module",
licenseFile: "LICENSE",
outDirectory: "dist/packages",
sourceRevision: revision,
target: "any",
@@ -33,6 +36,8 @@ describe("ekklesia-pack-module arguments", () => {
"any",
"--out",
"out",
"--license-file",
"LICENSE",
"--source-revision",
revision,
"--repository",
@@ -44,6 +49,10 @@ describe("ekklesia-pack-module arguments", () => {
it.each([
["no arguments", []],
["a missing target", ["--module", ".", "--out", "o", "--source-revision", revision]],
[
"a missing license file",
["--module", ".", "--target", "any", "--out", "o", "--source-revision", revision],
],
["a missing revision", ["--module", ".", "--target", "any", "--out", "o"]],
[
"an unknown option",
+61 -2
View File
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { readFile, symlink, writeFile } from "node:fs/promises";
import { isBuiltin } from "node:module";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
@@ -21,6 +21,7 @@ async function pack(options: FixtureModuleOptions = {}, overrides: Record<string
const moduleDirectory = await fixtureModule(options);
const outDirectory = await scratchDirectory("ekklesia-pack-out-");
const result = await packModule({
licenseFile: path.join(moduleDirectory, "LICENSE"),
moduleDirectory,
nodeRange: ">=24.11.0 <25",
outDirectory,
@@ -61,6 +62,7 @@ describe("packModule: what a package contains (ADR 0049)", () => {
const { archive } = await pack({ withSchema: true });
const files = [...fileEntries(archive).keys()].sort();
expect(files).toEqual([
"example/LICENSE",
"example/THIRD_PARTY_NOTICES",
expect.stringMatching(/^example\/dist\/chunks\/chunk-[A-Z0-9]+\.js$/u),
"example/dist/schema/index.js",
@@ -81,6 +83,62 @@ describe("packModule: what a package contains (ADR 0049)", () => {
);
});
it("copies the explicit license bytes unchanged and records their source SPDX expression", async () => {
const licenseText = Buffer.from([0x4d, 0x49, 0x54, 0x0d, 0x0a, 0x00, 0xff]);
const { archive } = await pack({ license: "MIT OR Apache-2.0", licenseText });
const files = fileEntries(archive);
const packaged = files.get("example/LICENSE");
const metadata = JSON.parse(
files.get("example/ekklesia.package.json")?.toString("utf8") ?? "{}",
) as {
license?: string;
files?: { path: string; size: number; sha256: string }[];
};
expect(metadata.license).toBe("MIT OR Apache-2.0");
expect(packaged?.equals(licenseText)).toBe(true);
expect(metadata.files?.filter((file) => file.path === "LICENSE")).toEqual([
{
path: "LICENSE",
size: licenseText.length,
sha256: createHash("sha256").update(licenseText).digest("hex"),
},
]);
});
it.each([
["a missing declaration", undefined],
["an empty declaration", ""],
["UNLICENSED", "UNLICENSED"],
["legacy object metadata", { type: "MIT", url: "https://example.invalid/license" }],
["a malformed SPDX expression", "MIT OR"],
])("refuses %s", async (_label, license) => {
await packRejection({ license }, "MODULE_PACK_MODULE_LICENSE_INVALID");
});
it("requires a readable regular license-text input", async () => {
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
licenseFile: undefined,
});
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
licenseFile: path.join(await scratchDirectory("ekklesia-missing-license-"), "missing"),
});
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
licenseFile: await scratchDirectory("ekklesia-license-directory-"),
});
const emptyDirectory = await scratchDirectory("ekklesia-empty-license-");
await writeFile(path.join(emptyDirectory, "LICENSE"), "");
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
licenseFile: path.join(emptyDirectory, "LICENSE"),
});
const linkDirectory = await scratchDirectory("ekklesia-linked-license-");
await writeFile(path.join(linkDirectory, "real"), "MIT License\n");
await symlink(path.join(linkDirectory, "real"), path.join(linkDirectory, "LICENSE"));
await packRejection({}, "MODULE_PACK_MODULE_LICENSE_FILE_INVALID", {
licenseFile: path.join(linkDirectory, "LICENSE"),
});
});
it("generates the runtime package.json instead of copying the development one", async () => {
const { archive } = await pack();
const runtimePackage = fileEntries(archive).get("example/package.json")?.toString("utf8");
@@ -420,11 +478,12 @@ describe("packModule: THIRD_PARTY_NOTICES", () => {
describe("packModule: deterministic archives", () => {
it("produces byte-identical archives and digests for identical inputs", async () => {
const moduleDirectory = await fixtureModule({ withSchema: true });
const runs = [];
for (let run = 0; run < 2; run += 1) {
const moduleDirectory = await fixtureModule({ withSchema: true });
const outDirectory = await scratchDirectory("ekklesia-pack-out-");
const result = await packModule({
licenseFile: path.join(moduleDirectory, "LICENSE"),
moduleDirectory,
nodeRange: ">=24.11.0 <25",
outDirectory,
@@ -24,6 +24,8 @@ export interface FakePackage {
export interface FixtureModuleOptions {
readonly databaseAbi?: 1;
readonly id?: string;
readonly license?: unknown;
readonly licenseText?: string | Uint8Array;
readonly version?: string;
readonly serverSource?: string;
readonly withModuleDatabase?: boolean;
@@ -114,6 +116,7 @@ export async function fixtureModule(options: FixtureModuleOptions = {}): Promise
{
name: `@test/module-${id}`,
version,
license: Object.hasOwn(options, "license") ? options.license : "MIT",
private: true,
type: "module",
scripts: { build: "tsc" },
@@ -130,6 +133,10 @@ export async function fixtureModule(options: FixtureModuleOptions = {}): Promise
2,
)}\n`,
);
await write(
path.join(moduleDirectory, "LICENSE"),
options.licenseText ?? "MIT License\n\nCopyright (c) 2026 Example Module\n",
);
await write(
path.join(moduleDirectory, "dist/server/index.js"),
options.serverSource ?? defaultServer(id, version, options.databaseAbi),
@@ -68,6 +68,7 @@ async function treeListing(directory: string): Promise<readonly string[]> {
async function packTemplate() {
const outDirectory = await scratchDirectory("ekklesia-template-package-");
const result = await packModule({
licenseFile: path.join(repositoryRoot, "LICENSE"),
moduleDirectory: await templateDirectory(),
outDirectory,
sourceRevision: SOURCE_REVISION,
@@ -104,8 +105,19 @@ describe("the module template as a Module Package v1", () => {
const files = fileEntries(archive);
const manifest = JSON.parse(files.get("example/ekklesia.module.json")?.toString("utf8") ?? "");
const metadata = JSON.parse(files.get("example/ekklesia.package.json")?.toString("utf8") ?? "");
const canonicalLicense = await readFile(path.join(repositoryRoot, "LICENSE"));
const packagedLicense = files.get("example/LICENSE");
expect(manifest.database).toEqual({ abi: 1 });
expect(metadata.license).toBe("AGPL-3.0-or-later");
expect(packagedLicense).toEqual(canonicalLicense);
expect(metadata.files.filter((file: { path: string }) => file.path === "LICENSE")).toEqual([
{
path: "LICENSE",
size: canonicalLicense.length,
sha256: createHash("sha256").update(canonicalLicense).digest("hex"),
},
]);
expect(metadata.transitional).toBeUndefined();
expect(metadata.incorporates).toMatchObject({
"@ekklesia/module-database": expect.any(String),
@@ -209,6 +221,12 @@ describe("the module template as a Module Package v1", () => {
new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&")} \\S+$`, "mu"),
);
}
expect(notices).not.toContain("@ekklesia/module-template");
expect(notices).not.toMatch(/^@ekklesia\//mu);
expect(bundled).not.toContain("spdx-expression-parse");
expect(bundled).not.toContain("spdx-exceptions");
expect(notices).not.toContain("spdx-expression-parse");
expect(notices).not.toContain("spdx-exceptions");
const licence = await readFile(path.join(packageRoot, "licenses", "Apache-2.0.txt"));
expect(createHash("sha256").update(licence).digest("hex")).toBe(
"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",