paperclipai/paperclip · critical · Error

Artifact URL does not match its content hash and trusted…

Error message

Artifact URL does not match its content hash and trusted origin.

What it means

The cloud migrator artifacts script throws this when a pin's integrity digest does not round-trip through base64 decoding, or the pin's URL does not equal the trusted artifactBase blob URL derived from the digest hex and expected extension. This ensures artifacts are only fetched from the trusted origin at the location matching their content hash, blocking substituted or relocated artifacts.

Solutions

  1. Regenerate the manifest so url and integrity are derived from the same content hash.
  2. Restore pin.url to ${artifactBase}/blobs/<sha512-hex>.<extension> for the correct extension (tgz for packages/lockfile... json for the lockfile).
  3. Verify artifactBase matches the environment the manifest was produced for.
  4. Audit how the manifest was modified — this error often indicates accidental or malicious rewriting.

Example fix

// before
"integrity": "sha512-AAAA...==", "url": "https://mirror.example.com/pkg.tgz"
// after
"integrity": "sha512-AAAA...==", "url": "<artifactBase>/blobs/<hex-of-sha512>.tgz"
Defensive patterns

Strategy: validation

Validate before calling

const digest = Buffer.from(pin.integrity.slice(7), 'base64');
const expectedUrl = `${artifactBase}/blobs/${digest.toString('hex')}.${extension}`;
if (digest.toString('base64') !== pin.integrity.slice(7) || pin.url !== expectedUrl)
  throw new Error('Refusing artifact: URL/integrity binding failed');

Type guard

const urlMatchesHash = (pin, base, ext) => pin.url === `${base}/blobs/${Buffer.from(pin.integrity.slice(7),'base64').toString('hex')}.${ext}`;

Try / catch

try { assertDescriptor(pin, 'tgz'); } catch (e) {
  if (e.message.includes('does not match its content hash')) {
    throw new Error('Possible tampering or stale mirror; re-download manifest from trusted origin');
  } else throw e;
}

Prevention

When it happens

Trigger: assertDescriptor is given a pin whose integrity is valid-format but whose bytes don't decode back to the same base64 (corrupted digest), or whose url was rewritten to a different host/path than ${artifactBase}/blobs/<hex>.<extension> (e.g. edited URL, mirror, or wrong extension).

Common situations: Redirecting artifacts to a local mirror without regenerating the manifest; copying integrity from one artifact into another row; a build pipeline changing the artifact base URL; tampered manifest.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/b570138c625dcdff. Report an issue: GitHub.

Appendix: source

Thrown at scripts/cloud-migrator-artifacts.mjs:29

export const artifactBase = "https://d1p6rlowie26tp.cloudfront.net/cloud-migrators/v1";
export const artifactBucket = "paperclipai-runner-e2e-history-078455283791-us-east-1";
const prefix = "cloud-migrators/v1/";
const names = ["db", "shared"];
const maximumBytes = 32 * 1024 * 1024;
const integrityFor = (bytes) => `sha512-${createHash("sha512").update(bytes).digest("base64")}`;

export function descriptor(bytes, extension) {
  const hash = createHash("sha512").update(bytes).digest("hex");
  return { url: `${artifactBase}/blobs/${hash}.${extension}`, integrity: integrityFor(bytes), size: bytes.length };
}

function assertDescriptor(pin, extension) {
  if (!pin || typeof pin.integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(pin.integrity) ||
      !Number.isSafeInteger(pin.size) || pin.size <= 0 || pin.size > maximumBytes) throw new Error("Invalid artifact integrity or size.");
  const digest = Buffer.from(pin.integrity.slice(7), "base64");
  if (digest.toString("base64") !== pin.integrity.slice(7) || pin.url !== `${artifactBase}/blobs/${digest.toString("hex")}.${extension}`) {
    throw new Error("Artifact URL does not match its content hash and trusted origin.");
  }
}

export function assertManifest(manifest, sha) {
  if (manifest?.version !== 1 || manifest.sourceSha !== sha || manifest.packageVersion !== versionFor(sha)) throw new Error("Artifact source identity mismatch.");
  for (const name of names) assertDescriptor(manifest.packages?.[name], "tgz");
  assertDescriptor(manifest.lockfile, "json");
}

export function assertLockfile(lock, manifest) {
  const version = manifest.packageVersion;
  if (lock?.lockfileVersion !== 3 || !lock.packages || Array.isArray(lock.packages) ||
      JSON.stringify(lock.packages[""]?.dependencies) !== JSON.stringify({ "@paperclipai/db": version })) throw new Error("Invalid migrator lockfile root.");
  for (const name of names) {
    const pin = lock.packages[`node_modules/@paperclipai/${name}`];
    const expected = manifest.packages[name];
    if (pin?.version !== version || pin.integrity !== expected.integrity || pin.resolved !== expected.url || pin.link || pin.inBundle) throw new Error("Migrator lockfile package pin mismatch.");
  }

View on GitHub (pinned to 3f1d897a7c)