{"record":{"id":"7d290bfdf882ff84","repo":"paperclipai/paperclip","slug":"migrator-lockfile-package-pin-mismatch","errorCode":null,"errorMessage":"Migrator lockfile package pin mismatch.","messagePattern":"Migrator lockfile package pin mismatch\\.","errorType":"console","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/cloud-migrator-artifacts.mjs","lineNumber":46,"sourceCode":"  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  }\n  if (lock.packages[\"node_modules/@paperclipai/db\"].dependencies?.[\"@paperclipai/shared\"] !== version) throw new Error(\"Migrator shared dependency mismatch.\");\n  for (const [key, entry] of Object.entries(lock.packages)) {\n    if (key === \"\") continue;\n    if (!entry || typeof entry !== \"object\" || entry.link) throw new Error(\"Invalid migrator lockfile entry.\");\n    if (/(?:^|\\/)node_modules\\/@paperclipai\\/[^/]+$/.test(key) && !names.some((name) => key === `node_modules/@paperclipai/${name}`)) throw new Error(\"Unexpected internal migrator dependency.\");\n    if (entry.inBundle === true) {\n      if (!key.startsWith(\"node_modules/@paperclipai/db/node_modules/\")) throw new Error(\"Unexpected bundled dependency.\");\n      continue;\n    }\n    if (!/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity ?? \"\")) throw new Error(\"Migrator dependency has no strong integrity pin.\");\n    if (names.some((name) => key === `node_modules/@paperclipai/${name}`)) continue;\n    const url = new URL(entry.resolved);\n    if (url.origin !== \"https://registry.npmjs.org\" || url.username || url.password || url.search || url.hash) throw new Error(\"Migrator dependency must resolve to npm.\");\n  }\n}\n\nexport function buildBundle(directory, sha, { exec = execFileSync } = {}) {","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/cloud-migrator-artifacts.mjs#L28-L64","documentation":"assertLockfile validates that the generated npm lockfile pins the internal @paperclipai/db and @paperclipai/shared packages to the exact version, integrity, and resolved URL recorded in the artifact manifest. This error is thrown when a node_modules/@paperclipai/<name> entry disagrees with the manifest on version, integrity hash, resolved URL, or is a link/bundled package. It is an internal supply-chain invariant: the lockfile must reference exactly the tgz artifacts whose bytes were verified.","triggerScenarios":"assertLockfile(lock, manifest) is called with a lockfile whose node_modules/@paperclipai/db or node_modules/@paperclipai/shared entry has a version !== manifest.packageVersion, an integrity or resolved URL differing from manifest.packages[name], or truthy link/inBundle flags.","commonSituations":"Hand-editing or regenerating package-lock.json after buildBundle rewrote the resolved URLs; npm re-resolution picking up a newly published npm version of @paperclipai/db instead of the local tarball; a stale lockfile from an older package version being validated against a new manifest; copying the lock between builds.","solutions":["Re-run `node scripts/cloud-migrator-artifacts.mjs build <dir> <sha>` so the lockfile is regenerated from the current tarballs and the resolved URLs/integrity are rewritten to match the manifest","Check that manifest.json and package-lock.json in the bundle directory come from the same build (same source SHA and package version)","Do not run `npm install`/`npm update` against the bundle directory after building; that re-resolves pins","Diff the offending node_modules/@paperclipai/<name> entry against manifest.packages[name] to see which field (version, integrity, resolved) diverged"],"exampleFix":"// before (lockfile drifted from manifest)\n\"node_modules/@paperclipai/db\": { \"version\": \"0.4.1\", \"resolved\": \"https://registry.npmjs.org/@paperclipai/db/-/db-0.4.1.tgz\", ... }\n// after (regenerated by buildBundle; points at immutable CDN blob)\n\"node_modules/@paperclipai/db\": { \"version\": \"0.4.2\", \"resolved\": \"https://d1p6rlowie26tp.cloudfront.net/cloud-migrators/v1/blobs/<sha512hex>.tgz\", \"integrity\": \"sha512-...\" }","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nconst lock = JSON.parse(readFileSync(\"package-lock.json\", \"utf8\"));\nconst manifest = JSON.parse(readFileSync(\"manifest.json\", \"utf8\"));\nfor (const name of [\"db\", \"shared\"]) {\n  const pin = lock.packages?.[`node_modules/@paperclipai/${name}`];\n  const exp = manifest.packages[name];\n  if (pin?.version !== manifest.packageVersion || pin?.integrity !== exp.integrity || pin?.resolved !== exp.url)\n    throw new Error(`lockfile pin for @paperclipai/${name} drifts from manifest`);\n}","typeGuard":"const pinMatches = (lock, manifest, name) => {\n  const pin = lock?.packages?.[`node_modules/@paperclipai/${name}`];\n  const exp = manifest?.packages?.[name];\n  return !!pin && pin.version === manifest.packageVersion && pin.integrity === exp.integrity && pin.resolved === exp.url && !pin.link && !pin.inBundle;\n};","tryCatchPattern":"try {\n  assertLockfile(lock, manifest);\n} catch (err) {\n  if (err.message === \"Migrator lockfile package pin mismatch.\") {\n    // regenerate the lockfile from the verified tarballs\n    buildBundle(dir, sha);\n  } else throw err;\n}","preventionTips":["Never hand-edit package-lock.json after buildBundle; always regenerate via the build command","Always build manifest.json and package-lock.json in the same buildBundle run","Do not run npm install/update inside the bundle directory after building","Diff lock pins against the manifest before publishing (validate command does this)"],"tags":["supply-chain","lockfile","integrity","validation"],"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"}