Files
openclaw/test/scripts/plugin-publication-artifact.test.ts

1767 lines
58 KiB
TypeScript

import { createHash } from "node:crypto";
/* oxlint-disable typescript/no-base-to-string -- fetch mocks normalize standard RequestInfo inputs exactly as the production fetch boundary does. */
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { deflateRawSync, gzipSync } from "node:zlib";
import * as tar from "tar";
import { afterEach, describe, expect, it } from "vitest";
import {
createPluginPublicationArtifact,
downloadActionsArtifactArchive,
inspectActionsArtifactZipWithPolicy,
verifyPluginPublicationArtifact,
} from "../../scripts/plugin-publication-artifact.mjs";
const TARGET_SHA = "1".repeat(40);
const WORKFLOW_SHA = "2".repeat(40);
const ARTIFACT_ID = 12345;
const RUN_ID = 67890;
const RUN_ATTEMPT = 2;
const REPOSITORY = "openclaw/openclaw";
const WORKFLOW_PATH = ".github/workflows/plugin-npm-release.yml";
const ARTIFACT_NAME = "plugin-npm-package-meta-2026.7.1-beta.3";
const PACKAGE_NAME = "@openclaw/meta-provider";
const PRODUCER_JOB_NAME = `Preflight plugin npm package (${PACKAGE_NAME})`;
const PACKAGE_VERSION = "2026.7.1-beta.3";
const PACKAGE_DIR = "extensions/meta";
const TARBALL_NAME = "openclaw-meta-provider-2026.7.1-beta.3.tgz";
const MANUAL_OVERRIDE_REASON =
"OpenClaw Release Publish run 12345 approved token release for v2026.7.1-beta.3";
const PUBLICATION_REASON = "First npm publication for the approved beta3 Meta package.";
const PUBLISHER_POLICY = {
policyId: "2026.7.1-beta.3",
schema: "openclaw.plugin-npm-publisher-policy/v1",
sha256: "6a40c33756ff1016744bb929660c1d9bf271cd478b0b9811fa8e2d8f1f775e95",
};
const tempDirs: string[] = [];
function tempDir(): string {
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-publication-artifact-"));
tempDirs.push(dir);
return dir;
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { force: true, recursive: true });
}
});
function sha256(bytes: Buffer): string {
return createHash("sha256").update(bytes).digest("hex");
}
function writeTarString(header: Buffer, offset: number, length: number, value: string): void {
const bytes = Buffer.from(value, "utf8");
if (bytes.length > length) {
throw new Error(`Tar field is too long: ${value}`);
}
bytes.copy(header, offset);
}
function writeTarOctal(header: Buffer, offset: number, length: number, value: number): void {
const raw = value.toString(8).padStart(length - 2, "0");
if (raw.length !== length - 2) {
throw new Error(`Tar number is too large: ${value}`);
}
writeTarString(header, offset, length, `${raw} \0`);
}
type TarEntry = {
content?: Buffer | string;
format?: "ustar" | "v7";
linkPath?: string;
path: string;
prefix?: string;
type?: "0" | "2" | "3" | "5" | "K" | "L" | "g" | "x";
};
function tarEntry(entry: TarEntry): Buffer {
const type = entry.type ?? "0";
const content = Buffer.isBuffer(entry.content)
? entry.content
: Buffer.from(entry.content ?? "", "utf8");
const header = Buffer.alloc(512);
writeTarString(header, 0, 100, entry.path);
writeTarOctal(header, 100, 8, type === "5" ? 0o755 : 0o644);
writeTarOctal(header, 108, 8, 0);
writeTarOctal(header, 116, 8, 0);
writeTarOctal(header, 124, 12, content.length);
writeTarOctal(header, 136, 12, 0);
header.fill(0x20, 148, 156);
header[156] = type.charCodeAt(0);
if (entry.linkPath) {
writeTarString(header, 157, 100, entry.linkPath);
}
if (entry.format !== "v7") {
writeTarString(header, 257, 6, "ustar\0");
writeTarString(header, 263, 2, "00");
}
writeTarOctal(header, 329, 8, 0);
writeTarOctal(header, 337, 8, 0);
if (entry.prefix) {
writeTarString(header, 345, 155, entry.prefix);
}
let checksum = 0;
for (const byte of header) {
checksum += byte;
}
writeTarOctal(header, 148, 8, checksum);
const padding = Buffer.alloc((512 - (content.length % 512)) % 512);
return Buffer.concat([header, content, padding]);
}
function mutateTarEntryHeader(
entry: Buffer,
mutate: (header: Buffer) => void,
options: { preserveChecksumBytes?: boolean } = {},
): Buffer {
const result = Buffer.from(entry);
const header = result.subarray(0, 512);
mutate(header);
if (!options.preserveChecksumBytes) {
header.fill(0x20, 148, 156);
let checksum = 0;
for (const byte of header) {
checksum += byte;
}
writeTarOctal(header, 148, 8, checksum);
}
return result;
}
function createTarball(entries: TarEntry[]): Buffer {
return gzipSync(Buffer.concat([...entries.map((entry) => tarEntry(entry)), Buffer.alloc(1024)]));
}
function createTarballFromParts(parts: Buffer[]): Buffer {
return gzipSync(Buffer.concat([...parts, Buffer.alloc(1024)]));
}
function paxRecord(key: string, value: string): Buffer {
const payload = `${key}=${value}\n`;
let length = Buffer.byteLength(payload) + 2;
while (true) {
const record = `${length} ${payload}`;
const actualLength = Buffer.byteLength(record);
if (actualLength === length) {
return Buffer.from(record, "utf8");
}
length = actualLength;
}
}
function crc32(bytes: Buffer): number {
let crc = 0xffffffff;
for (const byte of bytes) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
}
}
return (crc ^ 0xffffffff) >>> 0;
}
type ZipFile = {
bytes: Buffer;
centralFlags?: number;
compression?: 0 | 8;
compressedBytes?: Buffer;
declaredExpandedSize?: number;
descriptor?: boolean;
descriptorCrc?: number;
flags?: number;
gapAfter?: Buffer;
localCrc?: number;
localExpandedSize?: number;
localFlags?: number;
localNameBytes?: Buffer;
localCompressedSize?: number;
name: string;
nameBytes?: Buffer;
};
function createZip(files: ZipFile[]): Buffer {
const localParts: Buffer[] = [];
const centralParts: Buffer[] = [];
let localOffset = 0;
for (const file of files) {
const name = file.nameBytes ?? Buffer.from(file.name, "utf8");
const localName = file.localNameBytes ?? name;
const compression = file.compression ?? 0;
const compressed =
file.compressedBytes ?? (compression === 8 ? deflateRawSync(file.bytes) : file.bytes);
const expandedSize = file.declaredExpandedSize ?? file.bytes.length;
const checksum = crc32(file.bytes);
const flags = file.flags ?? (file.descriptor ? 0x0008 : 0);
const localFlags = file.localFlags ?? flags;
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0);
local.writeUInt16LE(20, 4);
local.writeUInt16LE(localFlags, 6);
local.writeUInt16LE(compression, 8);
local.writeUInt16LE(0, 10);
local.writeUInt16LE(0, 12);
local.writeUInt32LE(file.localCrc ?? (file.descriptor ? 0 : checksum), 14);
local.writeUInt32LE(file.localCompressedSize ?? (file.descriptor ? 0 : compressed.length), 18);
local.writeUInt32LE(file.localExpandedSize ?? (file.descriptor ? 0 : expandedSize), 22);
local.writeUInt16LE(localName.length, 26);
local.writeUInt16LE(0, 28);
const descriptor = file.descriptor
? (() => {
const value = Buffer.alloc(16);
value.writeUInt32LE(0x08074b50, 0);
value.writeUInt32LE(file.descriptorCrc ?? checksum, 4);
value.writeUInt32LE(compressed.length, 8);
value.writeUInt32LE(expandedSize, 12);
return value;
})()
: Buffer.alloc(0);
const gap = file.gapAfter ?? Buffer.alloc(0);
localParts.push(local, localName, compressed, descriptor, gap);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0);
central.writeUInt16LE(0x0314, 4);
central.writeUInt16LE(20, 6);
central.writeUInt16LE(file.centralFlags ?? flags, 8);
central.writeUInt16LE(compression, 10);
central.writeUInt16LE(0, 12);
central.writeUInt16LE(0, 14);
central.writeUInt32LE(checksum, 16);
central.writeUInt32LE(compressed.length, 20);
central.writeUInt32LE(expandedSize, 24);
central.writeUInt16LE(name.length, 28);
central.writeUInt16LE(0, 30);
central.writeUInt16LE(0, 32);
central.writeUInt16LE(0, 34);
central.writeUInt16LE(0, 36);
central.writeUInt32LE((0o100600 * 0x10000) >>> 0, 38);
central.writeUInt32LE(localOffset, 42);
centralParts.push(central, name);
localOffset +=
local.length + localName.length + compressed.length + descriptor.length + gap.length;
}
const centralDirectory = Buffer.concat(centralParts);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(0, 4);
end.writeUInt16LE(0, 6);
end.writeUInt16LE(files.length, 8);
end.writeUInt16LE(files.length, 10);
end.writeUInt32LE(centralDirectory.length, 12);
end.writeUInt32LE(localOffset, 16);
end.writeUInt16LE(0, 20);
return Buffer.concat([...localParts, centralDirectory, end]);
}
function inspectTestZip(
zip: Buffer,
overrides: Partial<{
maxArchiveBytes: number;
maxCompressedEntryBytes: (name: string) => number;
maxEntries: number;
maxExpandedBytes: number;
maxEntryBytes: (name: string) => number;
minEntries: number;
}> = {},
) {
return inspectActionsArtifactZipWithPolicy(zip, {
minEntries: 1,
maxEntries: 8,
maxArchiveBytes: 1024 * 1024,
maxExpandedBytes: 1024 * 1024,
allowPath: () => true,
maxCompressedEntryBytes: () => 1024 * 1024,
maxEntryBytes: () => 1024 * 1024,
...overrides,
});
}
function metaPackageJson(markerPath: string, overrides: Record<string, unknown> = {}): string {
return `${JSON.stringify(
{
name: PACKAGE_NAME,
version: PACKAGE_VERSION,
type: "module",
scripts: {
preinstall: `node -e "require('node:fs').writeFileSync(${JSON.stringify(markerPath)}, 'bad')"`,
postinstall: `node -e "require('node:fs').writeFileSync(${JSON.stringify(markerPath)}, 'bad')"`,
},
openclaw: {
release: {
publishToClawHub: true,
publishToNpm: true,
},
},
...overrides,
},
null,
2,
)}\n`;
}
function publicationParams(artifactDir: string, overrides: Record<string, unknown> = {}) {
const route = typeof overrides.route === "string" ? overrides.route : "npm-token-bootstrap";
const npmPolicy = route.startsWith("npm-")
? {
publicationReason: PUBLICATION_REASON,
publisherPolicy: PUBLISHER_POLICY,
}
: {};
return {
artifactDir,
artifactName: ARTIFACT_NAME,
packageDir: PACKAGE_DIR,
packageName: PACKAGE_NAME,
publishTag: "beta",
route,
sourcePackageJsonSha256: "3".repeat(64),
targetSha: TARGET_SHA,
version: PACKAGE_VERSION,
...npmPolicy,
...overrides,
};
}
function createFixture(
options: {
packageJson?: string;
publicationOverrides?: Record<string, unknown>;
tarEntries?: TarEntry[];
} = {},
) {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const outputDir = path.join(root, "verified");
const markerPath = path.join(root, "lifecycle-ran");
mkdirSync(artifactDir, { recursive: true });
const packageJson = options.packageJson ?? metaPackageJson(markerPath);
const tarball = createTarball(
options.tarEntries ?? [
{ path: "package/", type: "5" },
{ content: packageJson, path: "package/package.json" },
{ content: '{"id":"meta"}\n', path: "package/openclaw.plugin.json" },
{ content: "export default {};\n", path: "package/index.js" },
],
);
writeFileSync(path.join(artifactDir, TARBALL_NAME), tarball);
const created = createPluginPublicationArtifact(
publicationParams(artifactDir, options.publicationOverrides),
);
const manifestBytes = readFileSync(created.manifestPath);
const zip = createZip([
{ bytes: tarball, name: TARBALL_NAME },
{ bytes: manifestBytes, name: "plugin-publication-manifest.json" },
]);
const zipPath = path.join(root, "artifact.zip");
const metadataPath = path.join(root, "artifact.json");
const workflowRunPath = path.join(root, "run.json");
const workflowJobsPath = path.join(root, "jobs.json");
writeFileSync(zipPath, zip);
writeArtifactMetadata(metadataPath, zip);
writeWorkflowRunMetadata(workflowRunPath);
writeFileSync(
workflowJobsPath,
`${JSON.stringify({
total_count: 1,
jobs: [
{
name: PRODUCER_JOB_NAME,
run_id: RUN_ID,
run_attempt: RUN_ATTEMPT,
head_sha: WORKFLOW_SHA,
status: "completed",
conclusion: "success",
},
],
})}\n`,
);
return {
artifactDir,
created,
markerPath,
metadataPath,
outputDir,
publicationOverrides: options.publicationOverrides,
root,
tarball,
workflowRunPath,
workflowJobsPath,
zip,
zipPath,
};
}
function writeArtifactMetadata(metadataPath: string, zip: Buffer): void {
writeFileSync(
metadataPath,
`${JSON.stringify({
id: ARTIFACT_ID,
name: ARTIFACT_NAME,
expired: false,
digest: `sha256:${sha256(zip)}`,
size_in_bytes: zip.length,
workflow_run: {
id: RUN_ID,
head_sha: WORKFLOW_SHA,
},
})}\n`,
);
}
function writeWorkflowRunMetadata(workflowRunPath: string): void {
writeFileSync(
workflowRunPath,
`${JSON.stringify({
id: RUN_ID,
run_attempt: RUN_ATTEMPT,
head_sha: WORKFLOW_SHA,
head_branch: "main",
event: "workflow_dispatch",
path: WORKFLOW_PATH,
status: "completed",
conclusion: "success",
repository: { full_name: REPOSITORY },
head_repository: { full_name: REPOSITORY },
})}\n`,
);
}
function replaceArtifactZip(fixture: ReturnType<typeof createFixture>, files: ZipFile[]): void {
const zip = createZip(files);
fixture.zip = zip;
writeFileSync(fixture.zipPath, zip);
writeArtifactMetadata(fixture.metadataPath, zip);
}
function verifyFixture(
fixture: ReturnType<typeof createFixture>,
overrides: Record<string, unknown> = {},
) {
return verifyPluginPublicationArtifact({
...publicationParams(fixture.artifactDir, fixture.publicationOverrides),
artifactDigest: `sha256:${sha256(fixture.zip)}`,
artifactId: ARTIFACT_ID,
artifactMetadataPath: fixture.metadataPath,
artifactSizeBytes: fixture.zip.length,
artifactZipPath: fixture.zipPath,
outputDir: fixture.outputDir,
producerRunAttempt: RUN_ATTEMPT,
producerRunId: RUN_ID,
repository: REPOSITORY,
workflowEvent: "workflow_dispatch",
workflowHeadBranch: "main",
workflowPath: WORKFLOW_PATH,
workflowRunMetadataPath: fixture.workflowRunPath,
workflowSha: WORKFLOW_SHA,
...overrides,
});
}
describe("plugin publication artifact", () => {
it("canonically binds and verifies the Meta beta3 token-bootstrap tuple without running lifecycle scripts", () => {
const fixture = createFixture();
const verified = verifyFixture(fixture);
expect(verified.manifest).toMatchObject({
targetSha: TARGET_SHA,
package: {
dir: PACKAGE_DIR,
name: PACKAGE_NAME,
sourcePackageJsonSha256: "3".repeat(64),
version: PACKAGE_VERSION,
},
publication: {
authMode: "token-bootstrap",
capability: "first-publication",
publisherPolicy: PUBLISHER_POLICY,
reason: PUBLICATION_REASON,
route: "npm-token-bootstrap",
tag: "beta",
},
artifact: {
name: ARTIFACT_NAME,
npmIntegrity: `sha512-${createHash("sha512").update(fixture.tarball).digest("base64")}`,
npmShasum: createHash("sha1").update(fixture.tarball).digest("hex"),
sha256: sha256(fixture.tarball),
},
});
expect(verified).toMatchObject({
npmIntegrity: verified.manifest.artifact.npmIntegrity,
npmShasum: verified.manifest.artifact.npmShasum,
packageJsonSha256: verified.manifest.package.packageJsonSha256,
pluginManifestSha256: verified.manifest.package.pluginManifestSha256,
sourcePackageJsonSha256: "3".repeat(64),
tarballName: TARBALL_NAME,
});
expect(readFileSync(verified.tarballPath)).toEqual(fixture.tarball);
expect(verified.tarballInventory).toEqual(verified.manifest.artifact.inventory);
expect(verified.tarballSizeBytes).toBe(fixture.tarball.length);
expect(existsSync(fixture.markerPath)).toBe(false);
});
it("derives the closed npm auth capability and binds placeholder-recovery policy", () => {
const fixture = createFixture({
publicationOverrides: {
route: "npm-token-placeholder-recovery",
},
});
const verified = verifyFixture(fixture);
expect(verified.manifest.publication).toEqual({
authMode: "token-bootstrap",
capability: "placeholder-recovery",
publisherPolicy: PUBLISHER_POLICY,
reason: PUBLICATION_REASON,
route: "npm-token-placeholder-recovery",
tag: "beta",
});
expect(() =>
verifyFixture(fixture, {
authMode: "release-token",
}),
).toThrow("auth mode must be token-bootstrap");
expect(() =>
verifyFixture(fixture, {
capability: "first-publication",
}),
).toThrow("capability must be placeholder-recovery");
});
it("requires npm publication reason and exact publisher-policy identity", () => {
const fixture = createFixture();
for (const overrides of [
{ publicationReason: "" },
{ publicationReason: "invalid\nreason" },
{ publisherPolicy: undefined },
{ publisherPolicy: { ...PUBLISHER_POLICY, sha256: "A".repeat(64) } },
{ publisherPolicy: { ...PUBLISHER_POLICY, extra: true } },
]) {
expect(() => verifyFixture(fixture, overrides)).toThrow();
}
});
it("rejects a publication artifact bound to a different target package.json", () => {
const fixture = createFixture();
expect(() =>
verifyFixture(fixture, {
sourcePackageJsonSha256: "4".repeat(64),
}),
).toThrow("source package.json SHA-256 does not match the approved target source");
});
it("binds exact tarball size and canonical inventory", () => {
const fixture = createFixture();
const expectedInventory = JSON.parse(readFileSync(fixture.created.manifestPath, "utf8"))
.artifact.inventory;
expect(
verifyFixture(fixture, {
expectedInventory,
expectedTarballSha256: sha256(fixture.tarball),
expectedTarballSizeBytes: fixture.tarball.length,
}),
).toMatchObject({
tarballInventory: expectedInventory,
tarballSha256: sha256(fixture.tarball),
tarballSizeBytes: fixture.tarball.length,
});
const wrongSizeFixture = createFixture();
expect(() =>
verifyFixture(wrongSizeFixture, {
expectedTarballSizeBytes: wrongSizeFixture.tarball.length + 1,
}),
).toThrow("tarball size does not match");
const wrongInventoryFixture = createFixture();
expect(() =>
verifyFixture(wrongInventoryFixture, {
expectedInventory: expectedInventory.slice(0, -1),
}),
).toThrow("tarball inventory does not match");
});
it("requires a fresh non-symlink output directory", () => {
const existingFixture = createFixture();
mkdirSync(existingFixture.outputDir);
expect(() => verifyFixture(existingFixture)).toThrow("must not already exist");
const symlinkFixture = createFixture();
const symlinkTarget = path.join(symlinkFixture.root, "output-target");
mkdirSync(symlinkTarget);
symlinkSync(symlinkTarget, symlinkFixture.outputDir);
expect(() => verifyFixture(symlinkFixture)).toThrow("must not already exist");
});
it("binds the exact ClawHub token-release manual override reason", () => {
const fixture = createFixture({
publicationOverrides: {
manualOverrideReason: MANUAL_OVERRIDE_REASON,
requiresManualOverride: true,
route: "clawhub-token-release",
},
});
const verified = verifyFixture(fixture);
expect(verified.manifest.publication).toMatchObject({
manualOverrideReason: MANUAL_OVERRIDE_REASON,
requiresManualOverride: true,
route: "clawhub-token-release",
});
expect(() =>
verifyFixture(fixture, {
manualOverrideReason:
"OpenClaw Release Publish run 12345 approved token release for v2026.7.1-beta.4",
}),
).toThrow(/does not canonically bind/u);
});
it("requires a valid reason exactly when a ClawHub manual override is approved", () => {
const fixture = createFixture();
const invalidControls: Record<string, unknown>[] = [
{
requiresManualOverride: true,
route: "clawhub-token-release",
},
{
manualOverrideReason: MANUAL_OVERRIDE_REASON,
route: "clawhub-token-release",
},
{
manualOverrideReason: `${MANUAL_OVERRIDE_REASON}\nunsafe`,
requiresManualOverride: true,
route: "clawhub-token-release",
},
{
bootstrapMode: "publish",
manualOverrideReason: MANUAL_OVERRIDE_REASON,
requiresManualOverride: false,
route: "clawhub-token-bootstrap",
},
];
for (const controls of invalidControls) {
expect(() =>
verifyPluginPublicationArtifact({
...publicationParams(fixture.artifactDir, controls),
artifactDigest: `sha256:${sha256(fixture.zip)}`,
artifactId: ARTIFACT_ID,
artifactMetadataPath: fixture.metadataPath,
artifactZipPath: fixture.zipPath,
outputDir: fixture.outputDir,
runId: RUN_ID,
workflowSha: WORKFLOW_SHA,
}),
).toThrow();
}
});
it("rejects every mutable artifact identity dimension", () => {
const fixture = createFixture();
const mismatches: Array<[string, Record<string, unknown>]> = [
["artifact id", { artifactId: ARTIFACT_ID + 1 }],
["artifact name", { artifactName: "plugin-npm-package-other-2026.7.1-beta.3" }],
["artifact digest", { artifactDigest: `sha256:${"f".repeat(64)}` }],
["artifact size", { artifactSizeBytes: fixture.zip.length + 1 }],
["workflow run id", { producerRunId: RUN_ID + 1 }],
["workflow run attempt", { producerRunAttempt: RUN_ATTEMPT + 1 }],
["repository", { repository: "openclaw/not-openclaw" }],
["workflow SHA", { workflowSha: "3".repeat(40) }],
["workflow path", { workflowPath: ".github/workflows/other.yml" }],
["workflow event", { workflowEvent: "workflow_call" }],
["workflow head branch", { workflowHeadBranch: "release/2026.7.1" }],
["target SHA", { targetSha: "4".repeat(40) }],
["package name", { packageName: "@openclaw/not-meta" }],
["package dir", { packageDir: "extensions/not-meta" }],
["package version", { version: "2026.7.1-beta.2" }],
["publication route", { route: "npm-oidc" }],
["publish tag", { publishTag: "alpha" }],
];
for (const [label, overrides] of mismatches) {
expect(() => verifyFixture(fixture, overrides), label).toThrow();
}
});
it("binds authoritative workflow status, conclusion, and head repository", () => {
const mutations = [
(run: Record<string, unknown>) => {
run.status = "in_progress";
},
(run: Record<string, unknown>) => {
run.conclusion = "cancelled";
},
(run: Record<string, unknown>) => {
run.head_repository = { full_name: "openclaw/not-openclaw" };
},
];
for (const mutate of mutations) {
const fixture = createFixture();
const run = JSON.parse(readFileSync(fixture.workflowRunPath, "utf8"));
mutate(run);
writeFileSync(fixture.workflowRunPath, `${JSON.stringify(run)}\n`);
expect(() => verifyFixture(fixture)).toThrow(
/workflow run does not match the immutable publication tuple/u,
);
}
});
it("accepts only the exact successful producer job for same-run publication", () => {
const fixture = createFixture();
const workflowRun = JSON.parse(readFileSync(fixture.workflowRunPath, "utf8"));
workflowRun.status = "in_progress";
workflowRun.conclusion = null;
writeFileSync(fixture.workflowRunPath, `${JSON.stringify(workflowRun)}\n`);
expect(
verifyFixture(fixture, {
consumerRunAttempt: RUN_ATTEMPT,
producerJobName: PRODUCER_JOB_NAME,
runStatePolicy: "same-run-producer-success",
workflowJobsMetadataPath: fixture.workflowJobsPath,
}),
).toMatchObject({ producerRunAttempt: RUN_ATTEMPT, producerRunId: RUN_ID });
const jobs = JSON.parse(readFileSync(fixture.workflowJobsPath, "utf8"));
jobs.jobs[0].conclusion = "failure";
writeFileSync(fixture.workflowJobsPath, `${JSON.stringify(jobs)}\n`);
const failedFixture = createFixture();
writeFileSync(failedFixture.workflowRunPath, `${JSON.stringify(workflowRun)}\n`);
writeFileSync(failedFixture.workflowJobsPath, `${JSON.stringify(jobs)}\n`);
expect(() =>
verifyFixture(failedFixture, {
consumerRunAttempt: RUN_ATTEMPT,
producerJobName: PRODUCER_JOB_NAME,
runStatePolicy: "same-run-producer-success",
workflowJobsMetadataPath: failedFixture.workflowJobsPath,
}),
).toThrow("producer job did not complete successfully");
});
it("retries bounded metadata, attempt, and archive failures against the exact run attempt", async () => {
const zip = createZip([{ bytes: Buffer.from("proof"), name: "proof.txt" }]);
const artifactMetadata = {
id: ARTIFACT_ID,
name: ARTIFACT_NAME,
expired: false,
digest: `sha256:${sha256(zip)}`,
size_in_bytes: zip.length,
workflow_run: {
id: RUN_ID,
head_sha: WORKFLOW_SHA,
},
};
const workflowRun = {
id: RUN_ID,
run_attempt: RUN_ATTEMPT,
head_sha: WORKFLOW_SHA,
head_branch: "main",
event: "workflow_dispatch",
path: WORKFLOW_PATH,
status: "completed",
conclusion: "success",
repository: { full_name: REPOSITORY },
head_repository: { full_name: REPOSITORY },
};
const callCounts = { archive: 0, artifact: 0, run: 0 };
const urls: string[] = [];
const fetchImpl = (async (input: string | URL | Request) => {
const url = String(input);
urls.push(url);
if (url.endsWith(`/actions/artifacts/${ARTIFACT_ID}`)) {
callCounts.artifact += 1;
return callCounts.artifact === 1
? new Response("retry", { status: 503 })
: Response.json(artifactMetadata);
}
if (url.endsWith(`/actions/runs/${RUN_ID}/attempts/${RUN_ATTEMPT}`)) {
callCounts.run += 1;
return callCounts.run === 1
? new Response("{", { status: 200 })
: Response.json(workflowRun);
}
if (url.endsWith(`/actions/artifacts/${ARTIFACT_ID}/zip`)) {
callCounts.archive += 1;
return callCounts.archive === 1
? new Response("retry", { status: 502 })
: new Response(zip as unknown as BodyInit, {
status: 200,
headers: { "content-length": String(zip.length) },
});
}
return new Response("unexpected", { status: 404 });
}) as typeof fetch;
const result = await downloadActionsArtifactArchive({
expected: {
artifactDigest: `sha256:${sha256(zip)}`,
artifactId: ARTIFACT_ID,
artifactName: ARTIFACT_NAME,
artifactSizeBytes: zip.length,
repository: REPOSITORY,
runStatePolicy: "completed-success",
runAttempt: RUN_ATTEMPT,
runId: RUN_ID,
workflowEvent: "workflow_dispatch",
workflowHeadBranch: "main",
workflowPath: WORKFLOW_PATH,
workflowSha: WORKFLOW_SHA,
},
fetchImpl,
maxArchiveBytes: 1024 * 1024,
retryAttempts: 3,
retryDelayMs: 1,
token: "test-token",
});
expect(result.archiveBytes).toEqual(zip);
expect(callCounts).toEqual({ archive: 2, artifact: 2, run: 2 });
expect(urls).toContain(
`https://api.github.com/repos/${REPOSITORY}/actions/runs/${RUN_ID}/attempts/${RUN_ATTEMPT}`,
);
});
it("reuses only an exact successful producer job from the current or a prior attempt", async () => {
const zip = createZip([{ bytes: Buffer.from("proof"), name: "proof.txt" }]);
const artifactMetadata = {
id: ARTIFACT_ID,
name: ARTIFACT_NAME,
expired: false,
digest: `sha256:${sha256(zip)}`,
size_in_bytes: zip.length,
workflow_run: {
id: RUN_ID,
head_sha: WORKFLOW_SHA,
},
};
const producerJobName = "Pack immutable ClawHub bootstrap artifacts";
async function downloadForAttempts(
producerAttempt: number,
consumerAttempt: number,
producerConclusion = "success",
) {
const workflowRun = {
id: RUN_ID,
run_attempt: producerAttempt,
head_sha: WORKFLOW_SHA,
head_branch: "main",
event: "workflow_dispatch",
path: WORKFLOW_PATH,
status: producerAttempt === consumerAttempt ? "in_progress" : "completed",
conclusion: producerAttempt === consumerAttempt ? null : "failure",
repository: { full_name: REPOSITORY },
head_repository: { full_name: REPOSITORY },
};
const workflowJobs = {
total_count: 1,
jobs: [
{
name: producerJobName,
run_id: RUN_ID,
run_attempt: producerAttempt,
head_sha: WORKFLOW_SHA,
status: "completed",
conclusion: producerConclusion,
},
],
};
const fetchImpl = (async (input: string | URL | Request) => {
const url = String(input);
if (url.endsWith(`/actions/artifacts/${ARTIFACT_ID}`)) {
return Response.json(artifactMetadata);
}
if (url.endsWith(`/actions/runs/${RUN_ID}/attempts/${producerAttempt}`)) {
return Response.json(workflowRun);
}
if (url.endsWith(`/actions/runs/${RUN_ID}/attempts/${producerAttempt}/jobs?per_page=100`)) {
return Response.json(workflowJobs);
}
if (url.endsWith(`/actions/artifacts/${ARTIFACT_ID}/zip`)) {
return new Response(zip as unknown as BodyInit, {
status: 200,
headers: { "content-length": String(zip.length) },
});
}
return new Response("unexpected", { status: 404 });
}) as typeof fetch;
return downloadActionsArtifactArchive({
expected: {
artifactDigest: `sha256:${sha256(zip)}`,
artifactId: ARTIFACT_ID,
artifactName: ARTIFACT_NAME,
artifactSizeBytes: zip.length,
consumerRunAttempt: consumerAttempt,
producerJobName,
repository: REPOSITORY,
runStatePolicy: "same-run-producer-success",
runAttempt: producerAttempt,
runId: RUN_ID,
workflowEvent: "workflow_dispatch",
workflowHeadBranch: "main",
workflowPath: WORKFLOW_PATH,
workflowSha: WORKFLOW_SHA,
},
fetchImpl,
maxArchiveBytes: 1024 * 1024,
retryAttempts: 1,
token: "test-token",
});
}
await expect(downloadForAttempts(2, 2)).resolves.toMatchObject({
workflowJobs: { total_count: 1 },
});
await expect(downloadForAttempts(1, 2)).resolves.toMatchObject({
workflowRun: { conclusion: "failure", status: "completed" },
});
await expect(downloadForAttempts(1, 2, "failure")).rejects.toThrow(
"Actions artifact producer job did not complete successfully.",
);
await expect(downloadForAttempts(3, 2)).rejects.toThrow(
"Producer workflow run attempt must not be newer than the consumer attempt.",
);
});
it("rejects ZIP traversal, additional files, and byte tampering", () => {
const fixture = createFixture();
const manifest = readFileSync(fixture.created.manifestPath);
const malformedArtifacts: Array<{
expected: RegExp;
files: Array<{ bytes: Buffer; name: string }>;
}> = [
{
expected: /Unsafe ZIP entry path/u,
files: [
{ bytes: fixture.tarball, name: TARBALL_NAME },
{ bytes: manifest, name: "../plugin-publication-manifest.json" },
],
},
{
expected: /must contain between 2 and 2 exact files/u,
files: [
{ bytes: fixture.tarball, name: TARBALL_NAME },
{ bytes: manifest, name: "plugin-publication-manifest.json" },
{ bytes: Buffer.from("unexpected"), name: "extra.txt" },
],
},
];
for (const { expected, files } of malformedArtifacts) {
replaceArtifactZip(fixture, files);
expect(() => verifyFixture(fixture)).toThrow(expected);
}
const tamperFixture = createFixture();
const tampered = Buffer.from(tamperFixture.zip);
tampered[35] = tampered.readUInt8(35) ^ 0xff;
writeFileSync(tamperFixture.zipPath, tampered);
expect(() => verifyFixture(tamperFixture)).toThrow(/digest/u);
});
it("accepts canonical signed data descriptors and rejects noncanonical ZIP structure", () => {
const canonical = createZip([
{
bytes: Buffer.from("descriptor"),
compression: 8,
descriptor: true,
name: "descriptor.txt",
},
]);
expect(inspectTestZip(canonical).get("descriptor.txt")?.toString()).toBe("descriptor");
expect(() => inspectTestZip(Buffer.concat([canonical, Buffer.from("trailing")]))).toThrow(
/exact terminal end-of-central-directory/u,
);
expect(() =>
inspectTestZip(
createZip([
{
bytes: Buffer.from("gap"),
gapAfter: Buffer.from([0]),
name: "gap.txt",
},
]),
),
).toThrow(/gap or overlap/u);
expect(() =>
inspectTestZip(
createZip([
{
bytes: Buffer.from("crc"),
localCrc: 0,
name: "crc.txt",
},
]),
),
).toThrow(/local sizes or CRC/u);
expect(() =>
inspectTestZip(
createZip([
{
bytes: Buffer.from("descriptor"),
descriptor: true,
descriptorCrc: 0,
name: "descriptor.txt",
},
]),
),
).toThrow(/data descriptor/u);
});
it("rejects unsupported flags, invalid names, aliases, and trailing deflate bytes", () => {
for (const flags of [0x0040, 0x2000]) {
expect(() =>
inspectTestZip(createZip([{ bytes: Buffer.from("x"), flags, name: "flags.txt" }])),
).toThrow(/Unsupported Actions artifact ZIP flags/u);
}
expect(() =>
inspectTestZip(createZip([{ bytes: Buffer.from("x"), name: "m\u00e9ta.txt" }])),
).toThrow(/must set the UTF-8 language flag/u);
expect(
inspectTestZip(
createZip([{ bytes: Buffer.from("x"), flags: 0x0800, name: "m\u00e9ta.txt" }]),
).has("m\u00e9ta.txt"),
).toBe(true);
expect(() =>
inspectTestZip(
createZip([
{
bytes: Buffer.from("x"),
flags: 0x0800,
name: "invalid.txt",
nameBytes: Buffer.from([0xff]),
},
]),
),
).toThrow(/not valid UTF-8/u);
expect(() =>
inspectTestZip(
createZip([
{
bytes: Buffer.from("x"),
localNameBytes: Buffer.from("other.txt"),
name: "central.txt",
},
]),
),
).toThrow(/local and central names differ/u);
expect(() =>
inspectTestZip(
createZip([
{ bytes: Buffer.from("a"), name: "Case.txt" },
{ bytes: Buffer.from("b"), name: "case.txt" },
]),
),
).toThrow(/duplicate, or aliased/u);
const content = Buffer.from("deflate");
expect(() =>
inspectTestZip(
createZip([
{
bytes: content,
compressedBytes: Buffer.concat([deflateRawSync(content), Buffer.from([0, 1])]),
compression: 8,
name: "deflate.txt",
},
]),
),
).toThrow(/entry expansion exceeds/u);
expect(() =>
inspectTestZip(createZip([{ bytes: Buffer.from("compressed"), name: "cap.txt" }]), {
maxCompressedEntryBytes: () => 1,
}),
).toThrow(/entry is too large/u);
});
it("caps publication JSON and tarball members before ZIP expansion", () => {
const fixture = createFixture();
const manifest = readFileSync(fixture.created.manifestPath);
const oversizedManifest = Buffer.alloc(4 * 1024 * 1024 + 1, 0x20);
replaceArtifactZip(fixture, [
{ bytes: fixture.tarball, name: TARBALL_NAME },
{
bytes: oversizedManifest,
compression: 8,
name: "plugin-publication-manifest.json",
},
]);
expect(() => verifyFixture(fixture)).toThrow(
/entry is too large: plugin-publication-manifest\.json/u,
);
replaceArtifactZip(fixture, [
{ bytes: fixture.tarball, name: TARBALL_NAME },
{
bytes: oversizedManifest,
compression: 8,
declaredExpandedSize: 1,
name: "plugin-publication-manifest.json",
},
]);
expect(() => verifyFixture(fixture)).toThrow(
/entry expansion exceeds its allowed range: plugin-publication-manifest\.json/u,
);
replaceArtifactZip(fixture, [
{
bytes: fixture.tarball,
compression: 8,
declaredExpandedSize: 256 * 1024 * 1024 + 1,
name: TARBALL_NAME,
},
{ bytes: manifest, name: "plugin-publication-manifest.json" },
]);
expect(() => verifyFixture(fixture)).toThrow(
new RegExp(`entry is too large: ${TARBALL_NAME.replaceAll(".", "\\.")}`, "u"),
);
});
it("caps the number of tar headers before retaining their inventory", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ path: "package/", type: "5" },
{ content: metaPackageJson(markerPath), path: "package/package.json" },
...Array.from({ length: 10_000 }, (_, index) => ({
path: `package/file-${index.toString().padStart(5, "0")}`,
})),
]),
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/exceeds the 10000 entry limit/u,
);
});
it("rejects PAX metadata before retaining path inventory", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
const longPathPrefix = `package/${"a".repeat(900_000)}`;
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ path: "package/", type: "5" },
{ content: metaPackageJson(markerPath), path: "package/package.json" },
...Array.from({ length: 5 }, (_, index) => [
{
content: paxRecord("path", `${longPathPrefix}${index}`),
path: `PaxHeader-${index}`,
type: "x" as const,
},
{
path: `placeholder-${index}`,
},
]).flat(),
]),
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/PAX and GNU tar metadata are not supported/u,
);
});
it("rejects concatenated gzip members before trusting combined tar inventory", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
mkdirSync(artifactDir, { recursive: true });
const firstMember = gzipSync(
Buffer.concat([
tarEntry({ path: "package/", type: "5" }),
tarEntry({
content: metaPackageJson(markerPath),
path: "package/package.json",
}),
tarEntry({
content: '{"id":"meta"}\n',
path: "package/openclaw.plugin.json",
}),
]),
);
const secondMember = gzipSync(
Buffer.concat([
tarEntry({
content: "hidden from the pinned ClawHub reader\n",
path: "package/second-member.txt",
}),
Buffer.alloc(1024),
]),
);
writeFileSync(path.join(artifactDir, TARBALL_NAME), Buffer.concat([firstMember, secondMember]));
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/must contain exactly one gzip member/u,
);
});
it("rejects a hidden duplicate package.json after a single zero tar block", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
mkdirSync(artifactDir, { recursive: true });
const tarball = gzipSync(
Buffer.concat([
tarEntry({ path: "package/", type: "5" }),
tarEntry({
content: metaPackageJson(markerPath),
path: "package/package.json",
}),
Buffer.alloc(512),
tarEntry({
content: metaPackageJson(markerPath, {
scripts: {
postinstall: `node -e "require('node:fs').writeFileSync(${JSON.stringify(markerPath)}, 'hidden')"`,
},
}),
path: "package/package.json",
}),
Buffer.alloc(1024),
]),
);
writeFileSync(path.join(artifactDir, TARBALL_NAME), tarball);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/must end with two zero blocks and contain no trailing entries/u,
);
expect(existsSync(markerPath)).toBe(false);
});
it("rejects directory tar entries with nonzero declared size", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ content: "x", path: "package/", type: "5" },
{ content: metaPackageJson(markerPath), path: "package/package.json" },
]),
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/Directory tar entry "package\/" must have size zero/u,
);
});
it("rejects regular-file paths that the consumer coerces into directories", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
const tarballPath = path.join(artifactDir, TARBALL_NAME);
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
tarballPath,
createTarball([
{ path: "package/", type: "5" },
{
content: metaPackageJson(markerPath),
path: "package/package.json/",
},
]),
);
const consumerEntries: Array<{ path: string; size: number; type: string }> = [];
tar.t({
file: tarballPath,
onReadEntry: (entry) => {
consumerEntries.push({
path: entry.path,
size: entry.size,
type: entry.type,
});
},
onwarn: () => undefined,
sync: true,
});
expect(consumerEntries.at(-1)).toMatchObject({
path: "package/package.json/",
size: 0,
type: "Directory",
});
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/Non-directory tar entry must not end with a slash/u,
);
});
it.each([
{ path: " package.json", prefix: "package", field: "name" },
{ path: "package.json", prefix: " package", field: "prefix" },
])("rejects whitespace-bearing USTAR $field fields before manifest selection", (entry) => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ path: "package/", type: "5" },
{
content: metaPackageJson(markerPath, {
scripts: {
postinstall: `node -e "require('node:fs').writeFileSync(${JSON.stringify(markerPath)}, 'smuggled')"`,
},
}),
path: entry.path,
prefix: entry.prefix,
},
{ content: metaPackageJson(markerPath), path: "package/package.json" },
]),
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
new RegExp(
`tar entry ${entry.field} changes under the pinned ClawHub path normalization`,
"u",
),
);
expect(existsSync(markerPath)).toBe(false);
});
it("rejects V7 headers whose prefix bytes disagree with node-tar path semantics", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
const tarballPath = path.join(artifactDir, TARBALL_NAME);
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
tarballPath,
createTarball([
{ path: "package/", type: "5" },
{
content: metaPackageJson(markerPath),
format: "v7",
path: "package.json",
prefix: "package",
},
]),
);
const consumerPaths: string[] = [];
tar.t({
file: tarballPath,
onReadEntry: (entry) => consumerPaths.push(entry.path),
onwarn: () => undefined,
sync: true,
});
expect(consumerPaths).toContain("package.json");
expect(consumerPaths).not.toContain("package/package.json");
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/canonical POSIX USTAR headers/u,
);
});
it.each([
{
label: "regular-file link path",
mutate(header: Buffer) {
writeTarString(header, 157, 100, "package/alias");
},
message: /Plugin tar entries must not carry link targets/u,
},
{
label: "hidden link-path padding",
mutate(header: Buffer) {
header.fill(0, 157, 257);
Buffer.from("\0\nhidden-link", "utf8").copy(header, 157);
},
message: /tar entry link path has non-zero bytes after its NUL terminator/u,
},
{
label: "unterminated checksum",
mutate(header: Buffer) {
const checksum = Number.parseInt(header.subarray(148, 156).toString("ascii").trim(), 8);
writeTarString(header, 148, 8, checksum.toString(8).padStart(8, "0"));
},
preserveChecksumBytes: true,
message: /tar checksum is not canonically encoded/u,
},
...[
["mode", 100, 8, "tar entry mode"],
["uid", 108, 8, "tar entry uid"],
["gid", 116, 8, "tar entry gid"],
["mtime", 136, 12, "tar entry mtime"],
["device major", 329, 8, "tar entry device major"],
["device minor", 337, 8, "tar entry device minor"],
["access time", 476, 12, "tar entry access time"],
["change time", 488, 12, "tar entry change time"],
].map(([label, offset, length, field]) => ({
label: `invalid base-256 ${label}`,
mutate(header: Buffer) {
header.fill(0, offset as number, (offset as number) + (length as number));
header[offset as number] = 0x81;
},
message: new RegExp(`${field} must not use base-256 encoding`, "u"),
})),
])("rejects $label headers that make npm consume a nested manifest", (testCase) => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
const tarballPath = path.join(artifactDir, TARBALL_NAME);
mkdirSync(artifactDir, { recursive: true });
const nestedManifest = tarEntry({
content: metaPackageJson(markerPath, {
scripts: {
postinstall: `node -e "require('node:fs').writeFileSync(${JSON.stringify(markerPath)}, 'smuggled')"`,
},
tag: "latest",
}),
path: "package/package.json",
});
const decoy = mutateTarEntryHeader(
tarEntry({
content: nestedManifest,
path: "package/decoy",
}),
testCase.mutate,
{ preserveChecksumBytes: testCase.preserveChecksumBytes },
);
writeFileSync(
tarballPath,
createTarballFromParts([
tarEntry({ path: "package/", type: "5" }),
tarEntry({
content: metaPackageJson(markerPath),
path: "package/package.json",
}),
decoy,
]),
);
const consumerPaths: string[] = [];
tar.t({
file: tarballPath,
onReadEntry: (entry) => consumerPaths.push(entry.path),
onwarn: () => undefined,
sync: true,
});
expect(consumerPaths.filter((entryPath) => entryPath === "package/package.json")).toHaveLength(
2,
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
testCase.message,
);
expect(existsSync(markerPath)).toBe(false);
});
it("rejects PAX metadata containing control characters", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
const tarballPath = path.join(artifactDir, TARBALL_NAME);
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
tarballPath,
createTarball([
{ path: "package/", type: "5" },
{ content: metaPackageJson(markerPath), path: "package/package.json" },
{
content: paxRecord("comment", "benign\npath=package/package.json"),
path: "PaxHeader",
type: "x",
},
{
content: metaPackageJson(markerPath, {
scripts: {
postinstall: `node -e "require('node:fs').writeFileSync(${JSON.stringify(markerPath)}, 'injected')"`,
},
}),
path: "package/ignored.json",
},
]),
);
const consumerPaths: string[] = [];
tar.t({
file: tarballPath,
onReadEntry: (entry) => consumerPaths.push(entry.path),
onwarn: () => undefined,
sync: true,
});
expect(consumerPaths.filter((entryPath) => entryPath === "package/package.json")).toHaveLength(
1,
);
expect(consumerPaths).toContain("package/ignored.json");
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/PAX and GNU tar metadata are not supported/u,
);
expect(existsSync(markerPath)).toBe(false);
});
it("rejects local PAX and GNU metadata entries", () => {
const cases: TarEntry[][] = [
[
{ content: "package/ignored.json\0", path: "././@LongLink", type: "L" },
{
content: paxRecord("path", "package/package.json"),
path: "PaxHeader",
type: "x",
},
],
[
{
content: paxRecord("path", "package/ignored.json"),
path: "PaxHeader",
type: "x",
},
{ content: "package/package.json\0", path: "././@LongLink", type: "L" },
],
[
{
content: paxRecord("path", "package/package.json"),
path: "PaxHeader",
type: "x",
},
{ content: paxRecord("mtime", "0"), path: "PaxHeader2", type: "x" },
],
];
for (const [index, controls] of cases.entries()) {
const root = tempDir();
const artifactDir = path.join(root, `artifact-${index}`);
const markerPath = path.join(root, "marker");
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ path: "package/", type: "5" },
...controls,
{
content: metaPackageJson(markerPath),
path: `placeholder-${index}.json`,
},
]),
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/PAX and GNU tar metadata are not supported/u,
);
}
});
it("rejects local PAX size overrides", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ path: "package/", type: "5" },
{ content: metaPackageJson(markerPath), path: "package/package.json" },
{
content: paxRecord("size", "0"),
path: "PaxHeader",
type: "x",
},
{ content: "nonempty", path: "package/index.js" },
]),
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/PAX and GNU tar metadata are not supported/u,
);
});
it("rejects canonical PAX metadata for every plugin publication route", () => {
const root = tempDir();
const markerPath = path.join(root, "marker");
const longPath = `package/${"nested/".repeat(18)}index.js`;
const content = Buffer.from("export {};\n", "utf8");
const tarball = createTarball([
{ path: "package/", type: "5" },
{
content: metaPackageJson(markerPath),
path: "package/package.json",
},
{
content: Buffer.concat([
paxRecord("path", longPath),
paxRecord("size", String(content.length)),
]),
path: "PaxHeader",
type: "x",
},
{ content, path: "package/placeholder.js" },
]);
const npmArtifactDir = path.join(root, "npm-artifact");
mkdirSync(npmArtifactDir, { recursive: true });
writeFileSync(path.join(npmArtifactDir, TARBALL_NAME), tarball);
expect(() => createPluginPublicationArtifact(publicationParams(npmArtifactDir))).toThrow(
/PAX and GNU tar metadata are not supported/u,
);
for (const controls of [
{ route: "clawhub-token-release" },
{ bootstrapMode: "publish", route: "clawhub-token-bootstrap" },
{ route: "clawhub-readback" },
]) {
const { route } = controls;
const artifactDir = path.join(root, route);
mkdirSync(artifactDir, { recursive: true });
writeFileSync(path.join(artifactDir, TARBALL_NAME), tarball);
expect(() =>
createPluginPublicationArtifact(publicationParams(artifactDir, controls)),
).toThrow(/PAX and GNU tar metadata are not supported/u);
}
});
it("rejects oversized PAX metadata before parsing it", () => {
const root = tempDir();
const artifactDir = path.join(root, "artifact");
const markerPath = path.join(root, "marker");
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ path: "package/", type: "5" },
{
content: paxRecord("comment", "x".repeat(1024 * 1024)),
path: "PaxHeader",
type: "x",
},
{ content: metaPackageJson(markerPath), path: "package/package.json" },
]),
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/PAX and GNU tar metadata are not supported/u,
);
});
it("rejects beta npm artifacts bound to latest or extended-stable", () => {
const fixture = createFixture();
expect(() =>
createPluginPublicationArtifact(
publicationParams(fixture.artifactDir, { publishTag: "latest" }),
),
).toThrow(/does not match release channel beta/u);
expect(() =>
createPluginPublicationArtifact(
publicationParams(fixture.artifactDir, { publishTag: "extended-stable" }),
),
).toThrow(/Extended-stable npm publication requires/u);
});
it("rejects a package manifest tag that can override the approved npm dist-tag", () => {
const root = tempDir();
const markerPath = path.join(root, "marker");
const artifactDir = path.join(root, "artifact");
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ path: "package/", type: "5" },
{
content: metaPackageJson(markerPath, { tag: "latest" }),
path: "package/package.json",
},
{ content: '{"id":"meta"}\n', path: "package/openclaw.plugin.json" },
]),
);
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow(
/must not override the approved publication tag/u,
);
});
it("rejects publishConfig overrides in packed package metadata", () => {
expect(() =>
createFixture({
packageJson: metaPackageJson(path.join(tempDir(), "marker"), {
publishConfig: { tag: "beta" },
}),
}),
).toThrow(/must not override publication through publishConfig/u);
});
it("rejects traversal, links, and special entries inside the plugin tarball", () => {
const root = tempDir();
const markerPath = path.join(root, "marker");
for (const badEntry of [
{ content: "bad", path: "package/../escape" },
{ linkPath: "package/package.json", path: "package/alias", type: "2" as const },
{ path: "package/device", type: "3" as const },
]) {
const artifactDir = path.join(root, Math.random().toString(16).slice(2));
mkdirSync(artifactDir, { recursive: true });
const entries: TarEntry[] = [
{ path: "package/", type: "5" },
{ content: metaPackageJson(markerPath), path: "package/package.json" },
badEntry,
];
writeFileSync(path.join(artifactDir, TARBALL_NAME), createTarball(entries));
expect(() => createPluginPublicationArtifact(publicationParams(artifactDir))).toThrow();
}
expect(existsSync(markerPath)).toBe(false);
});
it("requires the exact Meta package name, dir, and both publication flags", () => {
const root = tempDir();
const markerPath = path.join(root, "marker");
const cases: Array<{
manifestOverrides?: Record<string, unknown>;
params?: Record<string, unknown>;
}> = [
{
manifestOverrides: {
openclaw: { release: { publishToClawHub: false, publishToNpm: true } },
},
},
{
manifestOverrides: {
openclaw: { release: { publishToClawHub: true, publishToNpm: false } },
},
},
{
manifestOverrides: { name: "@openclaw/not-meta" },
params: { packageName: "@openclaw/not-meta" },
},
{
params: { packageDir: "extensions/not-meta" },
},
];
for (const [index, testCase] of cases.entries()) {
const artifactDir = path.join(root, Math.random().toString(16).slice(2));
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, TARBALL_NAME),
createTarball([
{ path: "package/", type: "5" },
{
content: metaPackageJson(markerPath, testCase.manifestOverrides),
path: "package/package.json",
},
{ content: '{"id":"meta"}\n', path: "package/openclaw.plugin.json" },
]),
);
expect(
() => createPluginPublicationArtifact(publicationParams(artifactDir, testCase.params)),
`Meta identity case ${index}`,
).toThrow(/Meta publication requires extensions\/meta with npm and ClawHub enabled/u);
}
});
});