{"record":{"id":"b570138c625dcdff","repo":"paperclipai/paperclip","slug":"artifact-url-does-not-match-its-content-hash-and-trusted","errorCode":null,"errorMessage":"Artifact URL does not match its content hash and trusted origin.","messagePattern":"Artifact URL does not match its content hash and trusted origin\\.","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"scripts/cloud-migrator-artifacts.mjs","lineNumber":29,"sourceCode":"\nexport const artifactBase = \"https://d1p6rlowie26tp.cloudfront.net/cloud-migrators/v1\";\nexport const artifactBucket = \"paperclipai-runner-e2e-history-078455283791-us-east-1\";\nconst prefix = \"cloud-migrators/v1/\";\nconst names = [\"db\", \"shared\"];\nconst maximumBytes = 32 * 1024 * 1024;\nconst integrityFor = (bytes) => `sha512-${createHash(\"sha512\").update(bytes).digest(\"base64\")}`;\n\nexport function descriptor(bytes, extension) {\n  const hash = createHash(\"sha512\").update(bytes).digest(\"hex\");\n  return { url: `${artifactBase}/blobs/${hash}.${extension}`, integrity: integrityFor(bytes), size: bytes.length };\n}\n\nfunction assertDescriptor(pin, extension) {\n  if (!pin || typeof pin.integrity !== \"string\" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(pin.integrity) ||\n      !Number.isSafeInteger(pin.size) || pin.size <= 0 || pin.size > maximumBytes) throw new Error(\"Invalid artifact integrity or size.\");\n  const digest = Buffer.from(pin.integrity.slice(7), \"base64\");\n  if (digest.toString(\"base64\") !== pin.integrity.slice(7) || pin.url !== `${artifactBase}/blobs/${digest.toString(\"hex\")}.${extension}`) {\n    throw new Error(\"Artifact URL does not match its content hash and trusted origin.\");\n  }\n}\n\nexport function assertManifest(manifest, sha) {\n  if (manifest?.version !== 1 || manifest.sourceSha !== sha || manifest.packageVersion !== versionFor(sha)) throw new Error(\"Artifact source identity mismatch.\");\n  for (const name of names) assertDescriptor(manifest.packages?.[name], \"tgz\");\n  assertDescriptor(manifest.lockfile, \"json\");\n}\n\nexport function assertLockfile(lock, manifest) {\n  const version = manifest.packageVersion;\n  if (lock?.lockfileVersion !== 3 || !lock.packages || Array.isArray(lock.packages) ||\n      JSON.stringify(lock.packages[\"\"]?.dependencies) !== JSON.stringify({ \"@paperclipai/db\": version })) throw new Error(\"Invalid migrator lockfile root.\");\n  for (const name of names) {\n    const pin = lock.packages[`node_modules/@paperclipai/${name}`];\n    const expected = manifest.packages[name];\n    if (pin?.version !== version || pin.integrity !== expected.integrity || pin.resolved !== expected.url || pin.link || pin.inBundle) throw new Error(\"Migrator lockfile package pin mismatch.\");\n  }","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/cloud-migrator-artifacts.mjs#L11-L47","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Regenerate the manifest so url and integrity are derived from the same content hash.","Restore pin.url to ${artifactBase}/blobs/<sha512-hex>.<extension> for the correct extension (tgz for packages/lockfile... json for the lockfile).","Verify artifactBase matches the environment the manifest was produced for.","Audit how the manifest was modified — this error often indicates accidental or malicious rewriting."],"exampleFix":"// before\n\"integrity\": \"sha512-AAAA...==\", \"url\": \"https://mirror.example.com/pkg.tgz\"\n// after\n\"integrity\": \"sha512-AAAA...==\", \"url\": \"<artifactBase>/blobs/<hex-of-sha512>.tgz\"","handlingStrategy":"validation","validationCode":"const digest = Buffer.from(pin.integrity.slice(7), 'base64');\nconst expectedUrl = `${artifactBase}/blobs/${digest.toString('hex')}.${extension}`;\nif (digest.toString('base64') !== pin.integrity.slice(7) || pin.url !== expectedUrl)\n  throw new Error('Refusing artifact: URL/integrity binding failed');","typeGuard":"const urlMatchesHash = (pin, base, ext) => pin.url === `${base}/blobs/${Buffer.from(pin.integrity.slice(7),'base64').toString('hex')}.${ext}`;","tryCatchPattern":"try { assertDescriptor(pin, 'tgz'); } catch (e) {\n  if (e.message.includes('does not match its content hash')) {\n    throw new Error('Possible tampering or stale mirror; re-download manifest from trusted origin');\n  } else throw e;\n}","preventionTips":["Only download artifacts from the trusted artifactBase origin; never rewrite URLs to mirrors.","Regenerate the manifest whenever artifacts are republished or the base URL changes.","Treat any URL/hash mismatch as a security signal and investigate provenance."],"tags":["integrity","security","artifact","url"],"backgroundTag":"checksum-mismatch","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}