Files
Ekklesia/packages/module-pack/tests/pack-module.test.ts
T
ekklesiaandClaude Opus 5.5 44d385cd7f
CI / verify (pull_request) Successful in 1m6s
CI / infrastructure-smoke (pull_request) Successful in 27s
CI / migration-smoke (pull_request) Successful in 1m54s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 43s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
feat: native linux-x64-glibc module packages and reviewed license supplements (ADR 0049, ADR 0052)
Implements ADR 0049's deferred native target and ADR 0052's reviewed
license supplements. The proof case is a real module using sharp.
packageFormat stays 1, and `any` is unchanged.

Packager (@ekklesia/module-pack):
- --target linux-x64-glibc. Native or platform-specific packages, found by
  the same generic rule that refuses them in `any`, stay outside the bundle.
- Each native package's production closure is walked through its own
  dependencies, installed optionalDependencies that match the target, and
  required peers. It is copied as real files flat under node_modules/<name>,
  with no links, dev dependencies, declarations, source maps, or other
  platforms' packages. Missing or foreign-platform required dependencies,
  version conflicts, links, foreign machines, and Node.js range conflicts
  fail closed.
- runtime.nodeApi, nodeAbi, and libcVersion are read from the binaries by a
  small ELF reader: the addon's node_api_module_get_api_version_v1 and the
  newest GLIBC_ symbol version needed. No shelling out. A native target with
  no addon is refused.
- ADR 0052 supplements: package-provided material stays primary. An exact
  match on name, version, and ekklesia-content-v1 digest (sorted normalized
  paths with each file's size and SHA-256; no metadata) adds labelled
  material after the package's own. Any mismatch, a declared-licence
  conflict, or a malformed or unreviewed supplement file fails. Missing
  material with no supplement still fails.

Supplement for @img/[email protected]:
- 184 components and 143 unique texts, assembled from the exact upstream
  sources sharp-libvips v1.3.3 builds linux-x64 from: 27 C/C++ libraries,
  glib's gvdb, librsvg's Rust crates (reproduced with cargo tree), the Rust
  standard library at the rustc commit recorded in the binary, plus the
  LGPL-3.0, GPL-3.0, and MPL-2.0 texts. Each source is recorded with its
  SHA-256 in PROVENANCE.md.
- It pins digest ekklesia-content-v1:sha256:50647b0b…, which is identical
  from the installed copy and from the registry tarball.
- LGPL/MPL source-availability determinations stay open, as ADR 0052
  states. Nothing claims they are met.

Verifier (Core):
- linux-x64-glibc is accepted only on a host whose platform, arch, glibc
  family and version, and Node-API version (from Node itself) match.
- Every ELF file must be x86-64; every .node must be an ELF object.
- `any` packages now also refuse node_modules/ and ELF files.
- Contract: native metadata must state nodeApi or nodeAbi, and Linux targets
  libcVersion (ADR 0049).

Proof:
- A checked-in AGPL native fixture packs byte-identically from unrelated
  copies (8.86 MB) and verifies with Core's verifier.
- Extracted elsewhere, it runs a sharp resize and PNG round trip in an
  empty-environment process. Every resolution stays in the package, and
  only the package's addon and libvips plus host system libraries load.

Signage is untouched.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 20:16:53 -05:00

536 lines
20 KiB
TypeScript

import { createHash } from "node:crypto";
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";
import { ModulePackError, type ModulePackErrorCode, packModule } from "../src/index.js";
import {
cleanupFixtures,
type FixtureModuleOptions,
fixtureModule,
SOURCE_REVISION,
scratchDirectory,
} from "./support/fixture-module.js";
import { fileEntries, readTarGz } from "./support/tar.js";
afterEach(cleanupFixtures);
const MIT = "MIT License\n\nCopyright (c) 2026 Example Author\n\nPermission is hereby granted.\n";
async function pack(options: FixtureModuleOptions = {}, overrides: Record<string, unknown> = {}) {
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,
sourceRevision: SOURCE_REVISION,
target: "any",
...overrides,
});
return { archive: await readFile(result.archivePath), moduleDirectory, outDirectory, result };
}
async function packRejection(
options: FixtureModuleOptions,
code: ModulePackErrorCode,
overrides: Record<string, unknown> = {},
): Promise<ModulePackError> {
try {
await pack(options, overrides);
} catch (error) {
expect(error).toBeInstanceOf(ModulePackError);
expect((error as ModulePackError).code).toBe(code);
return error as ModulePackError;
}
throw new Error(`Expected ${code}.`);
}
describe("packModule: what a package contains (ADR 0049)", () => {
it("produces one archive and its sha256sum line, under the recommended name", async () => {
const { archive, outDirectory, result } = await pack();
const digest = createHash("sha256").update(archive).digest("hex");
expect(path.basename(result.archivePath)).toBe("example-0.1.0-any.tar.gz");
expect(result.sha256).toBe(digest);
expect(await readFile(path.join(outDirectory, "example-0.1.0-any.tar.gz.sha256"), "utf8")).toBe(
`${digest} example-0.1.0-any.tar.gz\n`,
);
});
it("holds exactly the runtime payload, under one top-level directory named with the module id", async () => {
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",
"example/dist/server/index.js",
"example/drizzle/0000_initial.sql",
"example/drizzle/meta/0000_snapshot.json",
"example/drizzle/meta/_ekklesia.json",
"example/drizzle/meta/_journal.json",
"example/ekklesia.module.json",
"example/ekklesia.package.json",
"example/package.json",
]);
for (const file of files) {
expect(file).not.toMatch(/\/src\/|\/tests\/|\.d\.ts$|\.map$|README|node_modules/u);
}
expect(new Set(readTarGz(archive).map((entry) => entry.path.split("/")[0]))).toEqual(
new Set(["example"]),
);
});
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");
expect(JSON.parse(runtimePackage ?? "")).toEqual({
name: "@test/module-example",
version: "0.1.0",
private: true,
type: "module",
ekklesia: { layer: "module" },
});
for (const [file, data] of fileEntries(archive)) {
if (file.endsWith(".json")) {
expect(data.toString("utf8"), file).not.toContain("workspace:");
}
}
});
it("copies the manifest byte for byte", async () => {
const { archive, moduleDirectory } = await pack();
expect(fileEntries(archive).get("example/ekklesia.module.json")).toEqual(
await readFile(path.join(moduleDirectory, "ekklesia.module.json")),
);
});
it("describes the package completely in ekklesia.package.json, never hashing itself", async () => {
const { archive } = await pack({ withSchema: true });
const files = fileEntries(archive);
const metadata = JSON.parse(files.get("example/ekklesia.package.json")?.toString("utf8") ?? "");
expect(metadata).toMatchObject({
packageFormat: 1,
module: { id: "example", version: "0.1.0" },
target: "any",
runtime: { node: ">=24.11.0 <25" },
provenance: {
revision: SOURCE_REVISION,
packager: { name: "@ekklesia/module-pack", version: expect.any(String) },
},
incorporates: {
"@ekklesia/contracts": expect.any(String),
"@ekklesia/module-sdk": expect.any(String),
},
transitional: { databaseRuntime: { "drizzle-orm": "0.45.2" } },
});
const listed = metadata.files.map((file: { path: string }) => `example/${file.path}`);
expect(listed).not.toContain("example/ekklesia.package.json");
expect(new Set(listed)).toEqual(
new Set([...files.keys()].filter((file) => file !== "example/ekklesia.package.json")),
);
for (const file of metadata.files as { path: string; size: number; sha256: string }[]) {
const data = files.get(`example/${file.path}`);
expect(data?.length).toBe(file.size);
expect(
createHash("sha256")
.update(data ?? "")
.digest("hex"),
).toBe(file.sha256);
}
});
it("records only the provenance it was given, and a repository only when asked", async () => {
const { archive } = await pack({}, { repository: "https://example.org/acme/example.git" });
const metadata = JSON.parse(
fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "",
);
expect(metadata.provenance.repository).toBe("https://example.org/acme/example.git");
});
it("omits transitional database facts from a module that bundles no Drizzle", async () => {
const { archive } = await pack({
serverSource: 'export default { id: "example", version: "0.1.0", register() {} };\n',
});
const metadata = JSON.parse(
fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "",
);
expect(metadata.transitional).toBeUndefined();
expect(metadata.incorporates).toEqual({});
});
});
describe("packModule: a self-contained backend", () => {
it("bundles the SDK, contracts, and Drizzle, leaving only Node built-ins to import", async () => {
const { archive } = await pack({ withSchema: true });
for (const [file, data] of fileEntries(archive)) {
if (!file.endsWith(".js")) {
continue;
}
const code = data.toString("utf8");
const specifiers = [
...code.matchAll(/(?:^|\n)\s*(?:import|export)\s[^;]*?from\s*"([^"]+)"/gu),
...code.matchAll(/(?:^|\n)\s*import\s*"([^"]+)"/gu),
...code.matchAll(/\bimport\(\s*"([^"]+)"\s*\)/gu),
].map((match) => match[1] ?? "");
for (const specifier of specifiers) {
expect(specifier.startsWith(".") || isBuiltin(specifier), `${file}: ${specifier}`).toBe(
true,
);
}
}
});
it("bundles ABI-v1 Drizzle as a module detail without transitional runtime metadata", async () => {
const { archive } = await pack({
databaseAbi: 1,
serverSource: `import { moduleDrizzle } from "@ekklesia/module-database/drizzle";
export const database = (executor) => moduleDrizzle(executor);
export default { database: { abi: 1 }, id: "example", version: "0.1.0", register() {} };
`,
withModuleDatabase: true,
});
const files = fileEntries(archive);
const metadata = JSON.parse(files.get("example/ekklesia.package.json")?.toString("utf8") ?? "");
expect(metadata.incorporates).toMatchObject({
"@ekklesia/module-database": expect.any(String),
});
expect(metadata.transitional).toBeUndefined();
for (const [file, data] of files) {
if (!file.endsWith(".js")) {
continue;
}
const code = data.toString("utf8");
const specifiers = [
...code.matchAll(/(?:^|\n)\s*(?:import|export)\s[^;]*?from\s*"([^"]+)"/gu),
...code.matchAll(/(?:^|\n)\s*import\s*"([^"]+)"/gu),
...code.matchAll(/\bimport\(\s*"([^"]+)"\s*\)/gu),
].map((match) => match[1] ?? "");
for (const specifier of specifiers) {
expect(specifier.startsWith(".") || isBuiltin(specifier), `${file}: ${specifier}`).toBe(
true,
);
}
}
});
it("keeps legacy-v8 Drizzle runtime metadata unchanged", async () => {
const { archive } = await pack({ withSchema: true });
const metadata = JSON.parse(
fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "",
);
expect(metadata.transitional).toEqual({ databaseRuntime: { "drizzle-orm": "0.45.2" } });
});
it("refuses a dependency the bundle cannot resolve rather than leaving it external", async () => {
await packRejection(
{
serverSource:
'import pad from "ekklesia-not-installed";\nexport default { id: "example", version: "0.1.0", register() { pad(); } };\n',
},
"MODULE_PACK_BUNDLE_FAILED",
);
});
it.each([
[
"a package that declares an operating system",
{
name: "fake-native",
packageJson: { os: ["linux"], cpu: ["x64"] },
files: { "index.js": "export default 1;\n" },
},
],
[
"a package built with node-gyp",
{
name: "fake-gyp",
packageJson: { gypfile: true },
files: { "index.js": "export default 1;\n", "binding.gyp": "{}" },
},
],
[
"a package that ships a compiled addon",
{
name: "fake-addon",
files: {
"index.js": "export default 1;\n",
"build/Release/addon.node": new Uint8Array([0x7f, 0x45, 0x4c, 0x46]),
},
},
],
])("refuses %s in an any-target package", async (_label, fake) => {
await packRejection(
{
fakePackages: [fake],
serverSource: `import value from "${fake.name}";\nexport default { id: "example", version: "0.1.0", register() { return value; } };\n`,
},
"MODULE_PACK_NATIVE_DEPENDENCY",
);
});
it("refuses a package whose installed dependency is platform-specific", async () => {
await packRejection(
{
fakePackages: [
{
name: "fake-wrapper",
packageJson: { optionalDependencies: { "fake-wrapper-linux-x64": "1.0.0" } },
files: { "index.js": "export default 1;\n", LICENSE: MIT },
},
{
name: "fake-wrapper-linux-x64",
packageJson: { os: ["linux"], cpu: ["x64"] },
files: { "index.js": "export default 2;\n" },
},
],
serverSource:
'import value from "fake-wrapper";\nexport default { id: "example", version: "0.1.0", register() { return value; } };\n',
},
"MODULE_PACK_NATIVE_DEPENDENCY",
);
});
it("refuses targets it does not produce, and a native target a pure-JavaScript module does not need", async () => {
await packRejection({}, "MODULE_PACK_TARGET_UNSUPPORTED", { target: "linux-arm64-glibc" });
await packRejection({}, "MODULE_PACK_NATIVE_TARGET_UNNEEDED", { target: "linux-x64-glibc" });
});
});
describe("packModule: identity and inputs", () => {
it("refuses a development package.json whose version differs from the manifest", async () => {
await packRejection(
{ manifest: (manifest) => ({ ...manifest, version: "0.2.0" }) },
"MODULE_PACK_IDENTITY_MISMATCH",
);
});
it("refuses a module whose backend was never built", async () => {
await packRejection(
{
manifest: (manifest) => ({
...manifest,
entrypoints: { server: "./dist/missing/index.js" },
}),
},
"MODULE_PACK_ENTRYPOINT_MISSING",
);
});
it("refuses an abbreviated or missing source revision", async () => {
await packRejection({}, "MODULE_PACK_REVISION_INVALID", { sourceRevision: "86ae907" });
});
it("refuses an incorporated SDK outside the manifest's own range", async () => {
await packRejection(
{
manifest: (manifest) => ({
...manifest,
compatibility: { platform: "^0.1.0", moduleSdk: "^9.0.0" },
}),
},
"MODULE_PACK_SDK_INCOMPATIBLE",
);
});
});
describe("packModule: THIRD_PARTY_NOTICES", () => {
const usesFake = (name: string) =>
`import value from "${name}";\nexport default { id: "example", version: "0.1.0", register() { return value; } };\n`;
it("credits every bundled third-party package once, with its licence and notice text", async () => {
const { archive } = await pack({
fakePackages: [
{
name: "fake-mit",
version: "2.3.4",
files: {
"index.js": 'import other from "fake-apache-notice";\nexport default other;\n',
LICENSE: MIT,
},
},
{
name: "fake-apache-notice",
version: "1.0.0",
packageJson: { license: "Apache-2.0" },
files: {
"index.js": "export default 1;\n",
"LICENSE.txt": "Apache License text shipped by the package.\n",
NOTICE: "Fake Apache Notice\nCopyright 2026 The Fake Foundation\n",
},
},
],
serverSource: usesFake("fake-mit"),
});
const notices = fileEntries(archive).get("example/THIRD_PARTY_NOTICES")?.toString("utf8") ?? "";
expect(notices).toContain("fake-mit 2.3.4");
expect(notices).toContain("Copyright (c) 2026 Example Author");
expect(notices).toContain("fake-apache-notice 1.0.0");
expect(notices).toContain("Copyright 2026 The Fake Foundation");
expect(notices.match(/^fake-mit 2\.3\.4$/gmu)).toHaveLength(1);
// Only what the bundle actually contains is credited: this backend uses no Drizzle code.
expect(notices).not.toContain("drizzle-orm");
// The Ekklesia platform's own packages are credited in ekklesia.package.json, not here.
expect(notices).not.toMatch(/^@ekklesia\//mu);
});
it("supplies the standard Apache-2.0 text only for a package that ships none, and says so", async () => {
const { archive } = await pack({ withSchema: true });
const notices = fileEntries(archive).get("example/THIRD_PARTY_NOTICES")?.toString("utf8") ?? "";
const drizzle = notices.slice(notices.indexOf("drizzle-orm 0.45.2"));
expect(drizzle).toContain("License: Apache-2.0");
expect(drizzle).toContain("ships no licence file");
expect(drizzle).toContain("TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION");
});
it("is byte-identical across packages of the same inputs", async () => {
const options = { withSchema: true };
const first = await pack(options);
const second = await pack(options);
expect(fileEntries(first.archive).get("example/THIRD_PARTY_NOTICES")).toEqual(
fileEntries(second.archive).get("example/THIRD_PARTY_NOTICES"),
);
});
it("refuses a bundled package that declares no licence", async () => {
await packRejection(
{
fakePackages: [
{
name: "fake-unlicensed",
packageJson: { license: undefined },
files: { "index.js": "export default 1;\n" },
},
],
serverSource: usesFake("fake-unlicensed"),
},
"MODULE_PACK_LICENCE_UNKNOWN",
);
});
it("refuses an MIT package that ships no licence text, because only it holds the copyright line", async () => {
await packRejection(
{
fakePackages: [{ name: "fake-textless", files: { "index.js": "export default 1;\n" } }],
serverSource: usesFake("fake-textless"),
},
"MODULE_PACK_LICENCE_TEXT_MISSING",
);
});
});
describe("packModule: deterministic archives", () => {
it("produces byte-identical archives and digests for identical inputs", async () => {
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,
sourceRevision: SOURCE_REVISION,
target: "any",
});
runs.push({ bytes: await readFile(result.archivePath), sha256: result.sha256 });
}
expect(runs[0]?.sha256).toBe(runs[1]?.sha256);
expect(runs[0]?.bytes.equals(runs[1]?.bytes ?? Buffer.alloc(0))).toBe(true);
});
it("normalizes every entry and the gzip header, and contains no links", async () => {
const { archive } = await pack({ withSchema: true });
// gzip: no file name flag, no modification time.
expect(archive[3]).toBe(0);
expect(archive.readUInt32LE(4)).toBe(0);
const entries = readTarGz(archive);
const paths = entries.map((entry) => entry.path);
expect(paths).toEqual(
[...paths].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0)),
);
for (const entry of entries) {
expect(["0", "5"], entry.path).toContain(entry.typeflag);
expect(entry.mode, entry.path).toBe(entry.typeflag === "5" ? 0o755 : 0o644);
expect([entry.uid, entry.gid, entry.mtime]).toEqual([0, 0, 0]);
expect([entry.uname, entry.gname]).toEqual(["", ""]);
expect(entry.magic).toBe("ustar/00");
}
// Every directory precedes what it contains.
for (const entry of entries) {
const parent = entry.path.split("/").slice(0, -1).join("/");
if (parent !== "") {
expect(paths.indexOf(parent), entry.path).toBeLessThan(paths.indexOf(entry.path));
}
}
});
it("refers to no absolute path of the build machine", async () => {
const { archive, moduleDirectory } = await pack({ withSchema: true });
const root = path.dirname(path.dirname(moduleDirectory));
for (const [file, data] of fileEntries(archive)) {
expect(data.toString("utf8"), file).not.toContain(moduleDirectory);
expect(data.toString("utf8"), file).not.toContain(root);
}
});
});