paperclipai/paperclip · error · Error

Invalid migrator lockfile root.

Error message

Invalid migrator lockfile root.

What it means

The cloud migrator artifacts script throws this from assertLockfile when the lockfile root is structurally wrong: lockfileVersion is not 3, packages is missing or is an array, or the root package "" does not have dependencies exactly { "@paperclipai/db": <version> }. It guarantees the lockfile being installed is the generated migrator lockfile and nothing else.

Solutions

  1. Use the generated migrator lockfile (lockfileVersion 3) from the artifact set, not an arbitrary package-lock.json.
  2. Regenerate the lockfile artifacts so the root depends exactly on @paperclipai/db at manifest.packageVersion.
  3. Check that the lockfile was not modified after download (npm install can rewrite it).
  4. Confirm manifest and lockfile come from the same artifact release.

Example fix

// before (rewritten root)
"packages": { "": { "dependencies": { "@paperclipai/db": "0.0.1", "extra": "..." } } }
// after
"packages": { "": { "dependencies": { "@paperclipai/db": "<manifest.packageVersion>" } } }
Defensive patterns

Strategy: validation

Validate before calling

const root = lock?.packages?.['']?.dependencies;
const isMigratorLock = lock?.lockfileVersion === 3 && lock.packages && !Array.isArray(lock.packages) &&
  JSON.stringify(root) === JSON.stringify({ '@paperclipai/db': manifest.packageVersion });
if (!isMigratorLock) throw new Error('Not the generated migrator lockfile');

Type guard

const isMigratorLockfile = (lock) => lock?.lockfileVersion === 3 && typeof lock.packages === 'object' && !Array.isArray(lock.packages);

Try / catch

try { assertLockfile(lock, manifest); } catch (e) {
  if (e.message === 'Invalid migrator lockfile root.') {
    console.error('Use the lockfile shipped in the artifact set, not a project package-lock.json');
  } else throw e;
}

Prevention

When it happens

Trigger: assertLockfile receives a lock object that is null, was produced by a different package manager/lockfileVersion (not 3), has packages as an array, or whose root package dependencies differ from the single expected @paperclipai/db dependency pinned to manifest.packageVersion.

Common situations: Pointing the migrator at a project's own package-lock.json instead of the generated migrator lockfile; a regenerated lockfile from a different manifest version; npm rewriting the root dependencies section.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

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.");
  }
  if (lock.packages["node_modules/@paperclipai/db"].dependencies?.["@paperclipai/shared"] !== version) throw new Error("Migrator shared dependency mismatch.");
  for (const [key, entry] of Object.entries(lock.packages)) {
    if (key === "") continue;
    if (!entry || typeof entry !== "object" || entry.link) throw new Error("Invalid migrator lockfile entry.");
    if (/(?:^|\/)node_modules\/@paperclipai\/[^/]+$/.test(key) && !names.some((name) => key === `node_modules/@paperclipai/${name}`)) throw new Error("Unexpected internal migrator dependency.");
    if (entry.inBundle === true) {
      if (!key.startsWith("node_modules/@paperclipai/db/node_modules/")) throw new Error("Unexpected bundled dependency.");
      continue;
    }
    if (!/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity ?? "")) throw new Error("Migrator dependency has no strong integrity pin.");
    if (names.some((name) => key === `node_modules/@paperclipai/${name}`)) continue;
    const url = new URL(entry.resolved);
    if (url.origin !== "https://registry.npmjs.org" || url.username || url.password || url.search || url.hash) throw new Error("Migrator dependency must resolve to npm.");

View on GitHub (pinned to 3f1d897a7c)