paperclipai/paperclip · error · Error
Migrator dependency must resolve to npm.
Error message
Migrator dependency must resolve to npm.
What it means
Non-internal, non-bundled lockfile entries must resolve from the official npm registry: the resolved URL's origin must be https://registry.npmjs.org with no credentials, query, or fragment. This error fires when a dependency resolves from a mirror, git URL, file path, or a URL carrying credentials/query strings — any of which would bypass the trusted-origin guarantee.
Solutions
- Set the registry to the official one for the build: run npm with --registry=https://registry.npmjs.org (as buildBundle does) and neutralize project .npmrc overrides
- Replace git:/file:/http: dependency specs in db/shared package.json with published npm versions, then re-lock
- Strip any credentials/query from resolved URLs by regenerating the lockfile rather than editing entries
Example fix
// before (built against mirror)
"some-dep": { "resolved": "https://npm.internal.corp/some-dep/-/some-dep-1.0.0.tgz" }
// after (built with --registry=https://registry.npmjs.org)
"some-dep": { "resolved": "https://registry.npmjs.org/some-dep/-/some-dep-1.0.0.tgz", "integrity": "sha512-..." } Defensive patterns
Strategy: validation
Validate before calling
const Official = "https://registry.npmjs.org";
for (const [key, entry] of Object.entries(lock.packages ?? {})) {
if (key === "" || entry?.inBundle === true) continue;
if (/node_modules\/@paperclipai\/(db|shared)$/.test(key)) continue;
const u = new URL(entry.resolved);
if (u.origin !== Official || u.username || u.password || u.search || u.hash)
throw new Error(`dependency ${key} resolves off official npm: ${entry.resolved}`);
} Type guard
const resolvesToOfficialNpm = (entry) => {
try { const u = new URL(entry?.resolved); return u.origin === "https://registry.npmjs.org" && !u.username && !u.password && !u.search && !u.hash; }
catch { return false; }
}; Try / catch
try {
assertLockfile(lock, manifest);
} catch (err) {
if (err.message === "Migrator dependency must resolve to npm.") throw new Error("Rebuild with --registry=https://registry.npmjs.org and without mirror/git deps");
throw err;
} Prevention
- Run builds with --registry=https://registry.npmjs.org and audit project .npmrc for mirror overrides
- Avoid git:/file:/URL dependency specs in db and shared package.json
- Regenerate (never hand-edit) resolved URLs
- Run `validate` in CI so off-registry resolution is caught before publish
When it happens
Trigger: assertLockfile computes new URL(entry.resolved) and the origin is not https://registry.npmjs.org, or username/password/search/hash are non-empty (e.g. resolved pointing at a GitHub tarball, a private registry mirror, or a URL like ...tgz?cache=bust).
Common situations: Building on a machine configured with a corporate npm mirror (.npmrc registry=...), a dependency specified as a git/file URL in package.json, or a proxy injecting query parameters into resolved URLs.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- Migrator dependency has no strong integrity pin.
- Invalid migrator lockfile entry.
- Materialized OpenCode executable digest mismatch
- Migrator lockfile package pin mismatch.
- Migrator shared dependency mismatch.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/96264bd7991440f5.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/cloud-migrator-artifacts.mjs:60
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.");
}
}
export function buildBundle(directory, sha, { exec = execFileSync } = {}) {
versionFor(sha);
directory = path.resolve(directory);
const packages = {};
for (const name of names) {
const bytes = readFileSync(path.join(directory, `${name}.tgz`));
assertMetadata(tarManifest(bytes), `@paperclipai/${name}`, sha);
packages[name] = descriptor(bytes, "tgz");
}
const scratch = mkdtempSync(path.join(os.tmpdir(), "cloud-migrator-lock-"));
try {
for (const name of names) copyFileSync(path.join(directory, `${name}.tgz`), path.join(scratch, `${name}.tgz`));
const root = { name: "paperclip-migrator-install-root", version: "0.0.0", private: true,
dependencies: { "@paperclipai/db": "file:db.tgz", "@paperclipai/shared": "file:shared.tgz" } };
writeFileSync(path.join(scratch, "package.json"), JSON.stringify(root));View on GitHub (pinned to 3f1d897a7c)