{"record":{"id":"3d02ee05c73dd750","repo":"Budibase/budibase","slug":"the-plugin-origin-must-be-from-npm","errorCode":null,"errorMessage":"The plugin origin must be from NPM","messagePattern":"The plugin origin must be from NPM","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/api/controllers/plugin/npm.ts","lineNumber":18,"sourceCode":"import { utils as coreUtils } from \"@budibase/backend-core\"\nimport {\n  deleteFolderFileSystem,\n  getPluginMetadata,\n} from \"../../../utilities/fileSystem\"\nimport { join } from \"path\"\nimport { downloadUnzipTarball } from \"./utils\"\n\nfunction parseNpmUrl(url: string): URL {\n  let parsed: URL\n  try {\n    parsed = new URL(url)\n  } catch {\n    throw new Error(\"Invalid NPM URL\")\n  }\n\n  if (parsed.protocol !== \"https:\") {\n    throw new Error(\"The plugin origin must be from NPM\")\n  }\n\n  return parsed\n}\n\nfunction isAllowedNpmHost(host: string): boolean {\n  return host === \"www.npmjs.com\" || host === \"registry.npmjs.org\"\n}\n\nexport async function npmUpload(url: string, name: string, headers = {}) {\n  let npmTarballUrl = url\n  let pluginName = name\n\n  const parsedInput = parseNpmUrl(npmTarballUrl)\n  if (!isAllowedNpmHost(parsedInput.hostname)) {\n    throw new Error(\"The plugin origin must be from NPM\")\n  }\n","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/plugin/npm.ts#L1-L36","documentation":"parseNpmUrl requires the https: protocol for npm plugin sources. Non-https URLs (http, git+ssh, registry:) are rejected even if otherwise well-formed; the actual npm host whitelist is enforced separately in npmUpload.","triggerScenarios":"npmUpload called with an http:// npm URL, a git+ssh:// or git+https:// URL, or any URL whose protocol is not exactly https:.","commonSituations":"Copying an http link from an internal mirror; using a tarball link from a private registry served over http; git+ssh URLs from package.json dependency entries.","solutions":["Switch the URL to https:// (e.g. https://registry.npmjs.org/<pkg>/-/<pkg>-<version>.tgz)","Download/publish the tarball on the public npm registry and use its https URL","If behind an http-only mirror, mirror the package to a https host"],"exampleFix":"// before\nconst url = 'http://registry.npmjs.org/my-plugin/-/my-plugin-1.0.0.tgz'\n// after\nconst url = 'https://registry.npmjs.org/my-plugin/-/my-plugin-1.0.0.tgz'","handlingStrategy":"validation","validationCode":"const u = new URL(url)\nif (u.protocol !== 'https:') throw new Error('NPM plugin URL must use https://')","typeGuard":"function isHttpsUrl(url: string): boolean {\n  try { return new URL(url).protocol === 'https:' } catch { return false }\n}","tryCatchPattern":"try {\n  await installPlugin({ source: 'NPM', url })\n} catch (err) {\n  if (err.message === 'The plugin origin must be from NPM') {\n    console.error('Rewrite the URL with https:// scheme')\n  }\n}","preventionTips":["Use https:// registry tarball URLs (https://registry.npmjs.org/...)","Convert git+ssh/git+https URLs to plain https","Avoid internal http-only mirrors as plugin sources"],"tags":["validation","url","npm"],"backgroundTag":"plugin-origin-not-allowed","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}