2019-01-29 14:43:00 -08:00
|
|
|
const { fixer } = require('normalize-package-data')
|
|
|
|
const npmFetch = require('npm-registry-fetch')
|
2020-10-02 17:52:19 -04:00
|
|
|
const npa = require('npm-package-arg')
|
2019-01-29 14:43:00 -08:00
|
|
|
const semver = require('semver')
|
2020-10-02 17:52:19 -04:00
|
|
|
const { URL } = require('url')
|
2019-01-29 14:43:00 -08:00
|
|
|
const ssri = require('ssri')
|
|
|
|
|
2020-11-03 20:39:24 -05:00
|
|
|
const publish = async (manifest, tarballData, opts) => {
|
2020-10-02 17:52:19 -04:00
|
|
|
if (manifest.private) {
|
|
|
|
throw Object.assign(
|
2020-11-03 20:39:24 -05:00
|
|
|
new Error(`This package has been marked as private
|
|
|
|
Remove the 'private' field from the package.json to publish it.`),
|
2020-10-02 17:52:19 -04:00
|
|
|
{ code: 'EPRIVATE' }
|
|
|
|
)
|
|
|
|
}
|
2019-01-29 14:43:00 -08:00
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
// spec is used to pick the appropriate registry/auth combo
|
|
|
|
const spec = npa.resolve(manifest.name, manifest.version)
|
|
|
|
opts = {
|
2022-12-06 22:18:33 -05:00
|
|
|
access: 'public',
|
2020-10-02 17:52:19 -04:00
|
|
|
algorithms: ['sha512'],
|
2022-12-06 22:18:33 -05:00
|
|
|
defaultTag: 'latest',
|
2020-10-02 17:52:19 -04:00
|
|
|
...opts,
|
2020-11-03 20:39:24 -05:00
|
|
|
spec,
|
2020-10-02 17:52:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2020-11-03 20:39:24 -05:00
|
|
|
ignoreBody: true,
|
2019-01-29 14:43:00 -08:00
|
|
|
})
|
2020-10-02 17:52:19 -04:00
|
|
|
} catch (err) {
|
2022-01-14 19:42:48 +02:00
|
|
|
if (err.code !== 'E409') {
|
2020-11-03 20:39:24 -05:00
|
|
|
throw err
|
2022-01-14 19:42:48 +02:00
|
|
|
}
|
2020-10-02 17:52:19 -04:00
|
|
|
// 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,
|
2020-11-03 20:39:24 -05:00
|
|
|
query: { write: true },
|
2020-10-02 17:52:19 -04:00
|
|
|
})
|
2022-02-24 21:41:49 +00:00
|
|
|
const newMetadata = patchMetadata(current, metadata)
|
2020-10-02 17:52:19 -04:00
|
|
|
return npmFetch(spec.escapedName, {
|
|
|
|
...opts,
|
|
|
|
method: 'PUT',
|
|
|
|
body: newMetadata,
|
2020-11-03 20:39:24 -05:00
|
|
|
ignoreBody: true,
|
2020-10-02 17:52:19 -04:00
|
|
|
})
|
|
|
|
}
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
|
2020-11-03 20:39:24 -05:00
|
|
|
const patchManifest = (_manifest, opts) => {
|
2020-10-02 17:52:19 -04:00
|
|
|
const { npmVersion } = opts
|
2020-11-03 20:39:24 -05:00
|
|
|
// we only update top-level fields, so a shallow clone is fine
|
|
|
|
const manifest = { ..._manifest }
|
2020-10-02 17:52:19 -04:00
|
|
|
|
2019-01-29 14:43:00 -08:00
|
|
|
manifest._nodeVersion = process.versions.node
|
2022-01-14 19:42:48 +02:00
|
|
|
if (npmVersion) {
|
2020-10-02 17:52:19 -04:00
|
|
|
manifest._npmVersion = npmVersion
|
2022-01-14 19:42:48 +02:00
|
|
|
}
|
2019-01-29 14:43:00 -08:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-11-03 20:39:24 -05:00
|
|
|
const buildMetadata = (registry, manifest, tarballData, opts) => {
|
2020-10-02 17:52:19 -04:00
|
|
|
const { access, defaultTag, algorithms } = opts
|
2019-01-29 14:43:00 -08:00
|
|
|
const root = {
|
|
|
|
_id: manifest.name,
|
|
|
|
name: manifest.name,
|
|
|
|
description: manifest.description,
|
|
|
|
'dist-tags': {},
|
|
|
|
versions: {},
|
2020-10-02 17:52:19 -04:00
|
|
|
access,
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
root.versions[manifest.version] = manifest
|
|
|
|
const tag = manifest.tag || defaultTag
|
2019-01-29 14:43:00 -08:00
|
|
|
root['dist-tags'][tag] = manifest.version
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
const tarballName = `${manifest.name}-${manifest.version}.tgz`
|
|
|
|
const tarballURI = `${manifest.name}/-/${tarballName}`
|
|
|
|
const integrity = ssri.fromData(tarballData, {
|
2020-11-03 20:39:24 -05:00
|
|
|
algorithms: [...new Set(['sha1'].concat(algorithms))],
|
2019-01-29 14:43:00 -08:00
|
|
|
})
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
manifest._id = `${manifest.name}@${manifest.version}`
|
|
|
|
manifest.dist = { ...manifest.dist }
|
2019-01-29 14:43:00 -08:00
|
|
|
// Don't bother having sha1 in the actual integrity field
|
2020-10-02 17:52:19 -04:00
|
|
|
manifest.dist.integrity = integrity.sha512[0].toString()
|
2019-01-29 14:43:00 -08:00
|
|
|
// Legacy shasum support
|
2020-10-02 17:52:19 -04:00
|
|
|
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
|
2019-01-29 14:43:00 -08:00
|
|
|
.replace(/^https:\/\//, 'http://')
|
|
|
|
|
|
|
|
root._attachments = {}
|
2020-10-02 17:52:19 -04:00
|
|
|
root._attachments[tarballName] = {
|
|
|
|
content_type: 'application/octet-stream',
|
|
|
|
data: tarballData.toString('base64'),
|
2020-11-03 20:39:24 -05:00
|
|
|
length: tarballData.length,
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return root
|
|
|
|
}
|
|
|
|
|
2020-11-03 20:39:24 -05:00
|
|
|
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))
|
2019-01-29 14:43:00 -08:00
|
|
|
|
|
|
|
const newVersion = Object.keys(newData.versions)[0]
|
|
|
|
|
|
|
|
if (curVers.indexOf(newVersion) !== -1) {
|
2020-10-02 17:52:19 -04:00
|
|
|
const { name: pkgid, version } = newData
|
|
|
|
throw Object.assign(
|
|
|
|
new Error(
|
|
|
|
`Cannot publish ${pkgid}@${version} over existing version.`
|
|
|
|
), {
|
|
|
|
code: 'EPUBLISHCONFLICT',
|
|
|
|
pkgid,
|
2020-11-03 20:39:24 -05:00
|
|
|
version,
|
2020-10-02 17:52:19 -04:00
|
|
|
})
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
current.versions = current.versions || {}
|
|
|
|
current.versions[newVersion] = newData.versions[newVersion]
|
2020-11-03 20:39:24 -05:00
|
|
|
for (const i in newData) {
|
2019-01-29 14:43:00 -08:00
|
|
|
switch (i) {
|
|
|
|
// objects that copy over the new stuffs
|
|
|
|
case 'dist-tags':
|
|
|
|
case 'versions':
|
|
|
|
case '_attachments':
|
2020-11-03 20:39:24 -05:00
|
|
|
for (const j in newData[i]) {
|
2019-01-29 14:43:00 -08:00
|
|
|
current[i] = current[i] || {}
|
|
|
|
current[i][j] = newData[i][j]
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
// copy
|
|
|
|
default:
|
|
|
|
current[i] = newData[i]
|
2020-11-03 20:39:24 -05:00
|
|
|
break
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-02 17:52:19 -04:00
|
|
|
return current
|
2019-01-29 14:43:00 -08:00
|
|
|
}
|
2020-11-03 20:39:24 -05:00
|
|
|
|
|
|
|
module.exports = publish
|