123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- const { fixer } = require('normalize-package-data')
- const npmFetch = require('npm-registry-fetch')
- const npa = require('npm-package-arg')
- const semver = require('semver')
- const { URL } = require('url')
- const ssri = require('ssri')
- const publish = async (manifest, tarballData, opts) => {
- if (manifest.private) {
- throw Object.assign(
- new Error(`This package has been marked as private
- Remove the 'private' field from the package.json to publish it.`),
- { code: 'EPRIVATE' }
- )
- }
- // spec is used to pick the appropriate registry/auth combo
- const spec = npa.resolve(manifest.name, manifest.version)
- opts = {
- defaultTag: 'latest',
- // if scoped, restricted by default
- access: spec.scope ? 'restricted' : 'public',
- algorithms: ['sha512'],
- ...opts,
- spec,
- }
- const reg = npmFetch.pickRegistry(spec, opts)
- const pubManifest = patchManifest(manifest, opts)
- // registry-frontdoor cares about the access level,
- // which is only configurable for scoped packages
- if (!spec.scope && opts.access === 'restricted') {
- throw Object.assign(
- new Error("Can't restrict access to unscoped packages."),
- { code: 'EUNSCOPED' }
- )
- }
- const metadata = buildMetadata(reg, pubManifest, tarballData, opts)
- try {
- return await npmFetch(spec.escapedName, {
- ...opts,
- method: 'PUT',
- body: metadata,
- ignoreBody: true,
- })
- } catch (err) {
- if (err.code !== 'E409')
- throw err
- // if E409, we attempt exactly ONE retry, to protect us
- // against malicious activity like trying to publish
- // a bunch of new versions of a package at the same time
- // and/or spamming the registry
- const current = await npmFetch.json(spec.escapedName, {
- ...opts,
- query: { write: true },
- })
- const newMetadata = patchMetadata(current, metadata, opts)
- return npmFetch(spec.escapedName, {
- ...opts,
- method: 'PUT',
- body: newMetadata,
- ignoreBody: true,
- })
- }
- }
- const patchManifest = (_manifest, opts) => {
- const { npmVersion } = opts
- // we only update top-level fields, so a shallow clone is fine
- const manifest = { ..._manifest }
- manifest._nodeVersion = process.versions.node
- if (npmVersion)
- manifest._npmVersion = npmVersion
- fixer.fixNameField(manifest, { strict: true, allowLegacyCase: true })
- const version = semver.clean(manifest.version)
- if (!version) {
- throw Object.assign(
- new Error('invalid semver: ' + manifest.version),
- { code: 'EBADSEMVER' }
- )
- }
- manifest.version = version
- return manifest
- }
- const buildMetadata = (registry, manifest, tarballData, opts) => {
- const { access, defaultTag, algorithms } = opts
- const root = {
- _id: manifest.name,
- name: manifest.name,
- description: manifest.description,
- 'dist-tags': {},
- versions: {},
- access,
- }
- root.versions[manifest.version] = manifest
- const tag = manifest.tag || defaultTag
- root['dist-tags'][tag] = manifest.version
- const tarballName = `${manifest.name}-${manifest.version}.tgz`
- const tarballURI = `${manifest.name}/-/${tarballName}`
- const integrity = ssri.fromData(tarballData, {
- algorithms: [...new Set(['sha1'].concat(algorithms))],
- })
- manifest._id = `${manifest.name}@${manifest.version}`
- manifest.dist = { ...manifest.dist }
- // Don't bother having sha1 in the actual integrity field
- manifest.dist.integrity = integrity.sha512[0].toString()
- // Legacy shasum support
- manifest.dist.shasum = integrity.sha1[0].hexDigest()
- // NB: the CLI always fetches via HTTPS if the registry is HTTPS,
- // regardless of what's here. This makes it so that installing
- // from an HTTP-only mirror doesn't cause problems, though.
- manifest.dist.tarball = new URL(tarballURI, registry).href
- .replace(/^https:\/\//, 'http://')
- root._attachments = {}
- root._attachments[tarballName] = {
- content_type: 'application/octet-stream',
- data: tarballData.toString('base64'),
- length: tarballData.length,
- }
- return root
- }
- const patchMetadata = (current, newData) => {
- const curVers = Object.keys(current.versions || {})
- .map(v => semver.clean(v, true))
- .concat(Object.keys(current.time || {})
- .map(v => semver.valid(v, true) && semver.clean(v, true))
- .filter(v => v))
- const newVersion = Object.keys(newData.versions)[0]
- if (curVers.indexOf(newVersion) !== -1) {
- const { name: pkgid, version } = newData
- throw Object.assign(
- new Error(
- `Cannot publish ${pkgid}@${version} over existing version.`
- ), {
- code: 'EPUBLISHCONFLICT',
- pkgid,
- version,
- })
- }
- current.versions = current.versions || {}
- current.versions[newVersion] = newData.versions[newVersion]
- for (const i in newData) {
- switch (i) {
- // objects that copy over the new stuffs
- case 'dist-tags':
- case 'versions':
- case '_attachments':
- for (const j in newData[i]) {
- current[i] = current[i] || {}
- current[i][j] = newData[i][j]
- }
- break
- // copy
- default:
- current[i] = newData[i]
- break
- }
- }
- return current
- }
- module.exports = publish
|