feat: native linux-x64-glibc module packages and reviewed license supplements (ADR 0049, ADR 0052)
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

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]>
This commit is contained in:
ekklesia
2026-10-07 20:16:53 -05:00
co-authored by Claude Opus 5.5
parent f53506b3f0
commit 44d385cd7f
173 changed files with 20638 additions and 53 deletions
+176 -1
View File
@@ -3,6 +3,8 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
currentModulePackageHost,
type ModulePackageHost,
ModulePackageVerificationError,
type ModulePackageVerificationErrorCode,
verifyModulePackage,
@@ -354,7 +356,7 @@ describe("Module Package v1 verification (ADR 0049)", () => {
metadata: (metadata) => ({
...metadata,
target: "linux-arm64-musl",
runtime: { node: ">=20", nodeApi: 9 },
runtime: { libcVersion: ">=1.2.0", node: ">=20", nodeApi: 9 },
}),
},
"MODULE_PACKAGE_TARGET_INCOMPATIBLE",
@@ -493,6 +495,179 @@ describe("Module Package v1 verification (ADR 0049)", () => {
});
});
describe("native targets (ADR 0049)", () => {
/** The first bytes of a 64-bit little-endian ELF object for `machine` (62 is x86-64). The
* verifier reads only the identification and machine fields. */
function elfHeader(machine = 62): Buffer {
const header = Buffer.alloc(64);
Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]).copy(header);
header.writeUInt16LE(3, 16);
header.writeUInt16LE(machine, 18);
return header;
}
const linuxHost = {
arch: "x64",
libc: { family: "glibc", version: "2.41" },
nodeApi: 10,
nodeAbi: "137",
platform: "linux",
} as const;
function nativePackage(
overrides: {
readonly files?: Record<string, Uint8Array | string>;
readonly target?: string;
readonly runtime?: Record<string, unknown>;
} = {},
): PackageFixtureOptions {
return {
files: overrides.files ?? {
"node_modules/native-thing/index.js": "module.exports = {};\n",
"node_modules/native-thing/lib/native-thing.node": elfHeader(),
"node_modules/native-thing/package.json": '{"name":"native-thing","version":"1.0.0"}\n',
},
metadata: (metadata) => ({
...metadata,
runtime: overrides.runtime ?? {
libcVersion: ">=2.28.0",
node: ">=20",
nodeApi: 9,
},
target: overrides.target ?? "linux-x64-glibc",
}),
};
}
async function verifyOn(
options: PackageFixtureOptions,
host: ModulePackageHost,
): Promise<unknown> {
const { archivePath } = await archiveFile(options);
return verifyModulePackage({ archivePath, platform: { ...platform, host } });
}
async function refusedOn(
options: PackageFixtureOptions,
host: ModulePackageHost,
code: ModulePackageVerificationErrorCode,
): Promise<void> {
await expect(verifyOn(options, host)).rejects.toMatchObject({ code });
}
it("accepts linux-x64-glibc on a matching host, native node_modules and addon included", async () => {
await expect(verifyOn(nativePackage(), linuxHost)).resolves.toMatchObject({
moduleId: "example",
target: "linux-x64-glibc",
});
});
it("refuses another operating system, CPU, or C library family", async () => {
await refusedOn(
nativePackage(),
{ ...linuxHost, platform: "darwin" },
"MODULE_PACKAGE_TARGET_INCOMPATIBLE",
);
await refusedOn(
nativePackage(),
{ ...linuxHost, arch: "arm64" },
"MODULE_PACKAGE_TARGET_INCOMPATIBLE",
);
// musl reports no glibc version, so the host cannot be proven glibc.
const { libc: _libc, ...musl } = linuxHost;
await refusedOn(nativePackage(), musl, "MODULE_PACKAGE_TARGET_INCOMPATIBLE");
});
it("refuses a target this platform does not implement", async () => {
await refusedOn(
nativePackage({ target: "linux-arm64-glibc" }),
{ ...linuxHost, arch: "arm64" },
"MODULE_PACKAGE_TARGET_INCOMPATIBLE",
);
});
it("refuses a C library older than the package needs", async () => {
await refusedOn(
nativePackage(),
{ ...linuxHost, libc: { family: "glibc", version: "2.17" } },
"MODULE_PACKAGE_LIBC_INCOMPATIBLE",
);
await expect(
verifyOn(nativePackage(), { ...linuxHost, libc: { family: "glibc", version: "2.28" } }),
).resolves.toMatchObject({ target: "linux-x64-glibc" });
});
it("refuses a Node-API version or Node.js ABI the host does not provide", async () => {
await refusedOn(
nativePackage(),
{ ...linuxHost, nodeApi: 8 },
"MODULE_PACKAGE_NODE_API_INCOMPATIBLE",
);
const { nodeApi: _nodeApi, ...unknownApi } = linuxHost;
await refusedOn(nativePackage(), unknownApi, "MODULE_PACKAGE_NODE_API_INCOMPATIBLE");
await refusedOn(
nativePackage({ runtime: { libcVersion: ">=2.28.0", node: ">=20", nodeAbi: "115" } }),
linuxHost,
"MODULE_PACKAGE_NODE_API_INCOMPATIBLE",
);
});
it("refuses a native file built for another machine, or an addon that is not one", async () => {
await refusedOn(
nativePackage({
files: { "node_modules/native-thing/lib/native-thing.node": elfHeader(183) },
}),
linuxHost,
"MODULE_PACKAGE_NATIVE_FILE_INCOMPATIBLE",
);
await refusedOn(
nativePackage({
files: { "node_modules/native-thing/lib/libthing.so": elfHeader(183) },
}),
linuxHost,
"MODULE_PACKAGE_NATIVE_FILE_INCOMPATIBLE",
);
await refusedOn(
nativePackage({
files: { "node_modules/native-thing/lib/native-thing.node": "not an ELF object" },
}),
linuxHost,
"MODULE_PACKAGE_NATIVE_FILE_INCOMPATIBLE",
);
});
it("requires a native package to state its Node.js interface and, on Linux, its C library", async () => {
await rejection(
nativePackage({ runtime: { libcVersion: ">=2.28.0", node: ">=20" } }),
"MODULE_PACKAGE_METADATA_INVALID",
);
await rejection(
nativePackage({ runtime: { node: ">=20", nodeApi: 9 } }),
"MODULE_PACKAGE_METADATA_INVALID",
);
});
it("keeps any-target packages free of node_modules, addons, and ELF objects", async () => {
await rejection(
{ files: { "node_modules/thing/index.js": "module.exports = {};\n" } },
"MODULE_PACKAGE_NATIVE_FILE",
);
await rejection({ files: { "dist/thing.node": elfHeader() } }, "MODULE_PACKAGE_NATIVE_FILE");
await rejection({ files: { "dist/libthing.so": elfHeader() } }, "MODULE_PACKAGE_NATIVE_FILE");
});
it("establishes this host's facts from Node.js alone", () => {
const host = currentModulePackageHost();
expect(host.platform).toBe(process.platform);
expect(host.arch).toBe(process.arch);
expect(host.nodeApi).toBe(Number(process.versions.napi));
if (process.platform === "linux") {
const report = process.report.getReport() as { header?: { glibcVersionRuntime?: string } };
expect(host.libc?.version).toBe(report.header?.glibcVersionRuntime);
}
});
});
it("fails an archive without package metadata", async () => {
const fixture = await modulePackageFixture();
const { gunzipSync, gzipSync } = await import("node:zlib");