{"record":{"id":"e71757286aa1ca50","repo":"paperclipai/paperclip","slug":"artifact-bytes-do-not-match-their-immutable-pin","errorCode":null,"errorMessage":"Artifact bytes do not match their immutable pin.","messagePattern":"Artifact bytes do not match their immutable pin\\.","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"scripts/cloud-migrator-artifacts.mjs","lineNumber":96,"sourceCode":"    writeFileSync(path.join(scratch, \"package.json\"), JSON.stringify(root));\n    exec(\"npm\", [\"install\", \"--package-lock-only\", \"--ignore-scripts\", \"--no-audit\", \"--no-fund\", \"--registry=https://registry.npmjs.org\"], { cwd: scratch, stdio: \"inherit\", timeout: 180_000 });\n    const lock = JSON.parse(readFileSync(path.join(scratch, \"package-lock.json\"), \"utf8\"));\n    // Both new packages are local during resolution. npm ci subsequently uses\n    // these immutable URLs, without looking up the new npm versions.\n    lock.packages[\"\"].dependencies = { \"@paperclipai/db\": versionFor(sha) };\n    for (const name of names) lock.packages[`node_modules/@paperclipai/${name}`].resolved = packages[name].url;\n    const lockBytes = Buffer.from(JSON.stringify(lock) + \"\\n\");\n    const manifest = { version: 1, sourceSha: sha, packageVersion: versionFor(sha), packages, lockfile: descriptor(lockBytes, \"json\") };\n    assertManifest(manifest, sha);\n    assertLockfile(lock, manifest);\n    writeFileSync(path.join(directory, \"package-lock.json\"), lockBytes);\n    writeFileSync(path.join(directory, \"manifest.json\"), JSON.stringify(manifest) + \"\\n\");\n    return manifest;\n  } finally { rmSync(scratch, { recursive: true, force: true }); }\n}\n\nfunction verifyBytes(bytes, pin) {\n  if (bytes.length !== pin.size || integrityFor(bytes) !== pin.integrity) throw new Error(\"Artifact bytes do not match their immutable pin.\");\n}\n\nexport function validateBundle(directory, sha) {\n  const manifest = JSON.parse(readFileSync(path.join(directory, \"manifest.json\"), \"utf8\"));\n  assertManifest(manifest, sha);\n  for (const name of names) {\n    const bytes = readFileSync(path.join(directory, `${name}.tgz`));\n    verifyBytes(bytes, manifest.packages[name]);\n    assertMetadata(tarManifest(bytes), `@paperclipai/${name}`, sha);\n  }\n  const bytes = readFileSync(path.join(directory, \"package-lock.json\"));\n  verifyBytes(bytes, manifest.lockfile);\n  assertLockfile(JSON.parse(bytes), manifest);\n  return manifest;\n}\n\n/** Exercise the real dependency graph before publishing, with no new npm versions. */\nexport function verifyInstall(directory, sha, { exec = execFileSync } = {}) {","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/cloud-migrator-artifacts.mjs#L78-L114","documentation":"verifyBytes compares downloaded or on-disk artifact bytes against the manifest's immutable pin (exact size and sha512 integrity). This error means the bytes diverge from what the manifest pins — the file was corrupted, truncated, replaced, or the manifest describes a different build. It is the content-addressing integrity gate for the whole bundle.","triggerScenarios":"verifyBytes(bytes, pin) is called from validateBundle (local files) or verifyPublished (downloads) and either bytes.length !== pin.size or integrityFor(bytes) !== pin.integrity.","commonSituations":"A partially failed download or truncated tgz on disk; CDN serving a stale cached blob under an address that was overwritten; editing a tgz/json after the manifest was written; mixing files from two different builds of the same SHA.","solutions":["Re-download or re-fetch the bundle fresh (rm the local artifacts and re-run the build or verify) so bytes and manifest come from the same run","Rebuild the bundle with `node scripts/cloud-migrator-artifacts.mjs build <dir> <sha>` so manifest pins match current bytes","Confirm the CDN/S3 object was not overwritten (uploads use --if-none-match * for immutability); if it was, treat the artifact as compromised and re-publish under the correct hash","Check the directory contains files from a single build (manifest.json, package-lock.json, db.tgz, shared.tgz all same SHA)"],"exampleFix":"// before (file edited after manifest written)\nverifyBytes(bytes, pin) -> sha512(actual) != pin.integrity\n// after (rebuild so pins match bytes)\nnode scripts/cloud-migrator-artifacts.mjs build ./bundle <sha>\nnode scripts/cloud-migrator-artifacts.mjs validate ./bundle <sha>","handlingStrategy":"validation","validationCode":"import { createHash } from \"node:crypto\";\nconst integrityFor = (b) => `sha512-${createHash(\"sha512\").update(b).digest(\"base64\")}`;\nconst bytes = readFileSync(path.join(dir, \"db.tgz\"));\nconst pin = manifest.packages.db;\nif (bytes.length !== pin.size || integrityFor(bytes) !== pin.integrity) throw new Error(\"db.tgz does not match manifest pin\");","typeGuard":"const matchesPin = (bytes, pin) =>\n  Buffer.isBuffer(bytes) && bytes.length === pin.size && integrityFor(bytes) === pin.integrity;","tryCatchPattern":"try {\n  validateBundle(dir, sha);\n} catch (err) {\n  if (err.message === \"Artifact bytes do not match their immutable pin.\") {\n    // bytes and manifest disagree: rebuild or re-download from scratch\n    rmSync(path.join(dir, \"db.tgz\"));\n    buildBundle(dir, sha); // or re-run verifyPublished to re-download\n  } else throw err;\n}","preventionTips":["Never modify files inside a built bundle; the manifest pins are immutable","Discard and rebuild a bundle after any failed/partial write","Trust only content-addressed URLs (hash-in-path) when downloading","Keep all four bundle files (manifest, lock, two tgzs) from one build run"],"tags":["integrity","checksum","supply-chain","tampering"],"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"}