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' )
2024-04-30 23:53:22 -07:00
const { log } = require ( 'proc-log' )
2019-01-29 14:43:00 -08:00
const semver = require ( 'semver' )
2024-05-30 04:21:05 -07:00
const { URL } = require ( 'node:url' )
2019-01-29 14:43:00 -08:00
const ssri = require ( 'ssri' )
2023-02-18 17:09:39 -05:00
const ciInfo = require ( 'ci-info' )
2023-06-08 05:24:49 -07:00
const { generateProvenance , verifyProvenance } = require ( './provenance' )
2019-01-29 14:43:00 -08:00
2023-03-31 06:38:48 -07:00
const TLOG _BASE _URL = 'https://search.sigstore.dev/'
2023-03-15 17:39:48 +00:00
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' }
)
}
2023-05-19 06:45:02 -07:00
const { metadata , transparencyLogUrl } = await buildMetadata (
reg ,
pubManifest ,
tarballData ,
spec ,
opts
)
2020-10-02 17:52:19 -04:00
2023-09-06 12:54:44 -07:00
const res = await npmFetch ( spec . escapedName , {
... opts ,
method : 'PUT' ,
body : metadata ,
ignoreBody : true ,
} )
if ( transparencyLogUrl ) {
res . transparencyLogUrl = transparencyLogUrl
2020-10-02 17:52:19 -04:00
}
2023-09-06 12:54:44 -07:00
return res
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
}
2023-02-18 17:09:39 -05:00
const buildMetadata = async ( registry , manifest , tarballData , spec , opts ) => {
2023-06-08 05:24:49 -07:00
const { access , defaultTag , algorithms , provenance , provenanceFile } = 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 `
2023-02-18 17:09:39 -05:00
const provenanceBundleName = ` ${ manifest . name } - ${ manifest . version } .sigstore `
2020-10-02 17:52:19 -04:00
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
}
2023-02-18 17:09:39 -05:00
// Handle case where --provenance flag was set to true
2023-05-19 06:45:02 -07:00
let transparencyLogUrl
2023-06-08 05:24:49 -07:00
if ( provenance === true || provenanceFile ) {
let provenanceBundle
2023-02-18 17:09:39 -05:00
const subject = {
name : npa . toPurl ( spec ) ,
digest : { sha512 : integrity . sha512 [ 0 ] . hexDigest ( ) } ,
}
2023-06-08 05:24:49 -07:00
if ( provenance === true ) {
await ensureProvenanceGeneration ( registry , spec , opts )
2025-01-10 08:20:27 -08:00
provenanceBundle = await generateProvenance ( [ subject ] , opts )
2023-06-08 05:24:49 -07:00
/* eslint-disable-next-line max-len */
2023-06-22 07:48:43 -07:00
log . notice ( 'publish' , ` Signed provenance statement with source and build information from ${ ciInfo . name } ` )
2023-06-08 05:24:49 -07:00
const tlogEntry = provenanceBundle ? . verificationMaterial ? . tlogEntries [ 0 ]
/* istanbul ignore else */
if ( tlogEntry ) {
transparencyLogUrl = ` ${ TLOG _BASE _URL } ?logIndex= ${ tlogEntry . logIndex } `
log . notice (
'publish' ,
` Provenance statement published to transparency log: ${ transparencyLogUrl } `
)
2023-05-19 06:45:02 -07:00
}
2023-06-08 05:24:49 -07:00
} else {
provenanceBundle = await verifyProvenance ( subject , provenanceFile )
2023-03-15 17:39:48 +00:00
}
2023-02-18 17:09:39 -05:00
const serializedBundle = JSON . stringify ( provenanceBundle )
root . _attachments [ provenanceBundleName ] = {
content _type : provenanceBundle . mediaType ,
data : serializedBundle ,
length : serializedBundle . length ,
}
}
2023-05-19 06:45:02 -07:00
return {
metadata : root ,
transparencyLogUrl ,
}
2019-01-29 14:43:00 -08:00
}
2023-06-08 05:24:49 -07:00
// Check that all the prereqs are met for provenance generation
const ensureProvenanceGeneration = async ( registry , spec , opts ) => {
2023-06-22 07:48:43 -07:00
if ( ciInfo . GITHUB _ACTIONS ) {
// Ensure that the GHA OIDC token is available
if ( ! process . env . ACTIONS _ID _TOKEN _REQUEST _URL ) {
throw Object . assign (
/* eslint-disable-next-line max-len */
new Error ( 'Provenance generation in GitHub Actions requires "write" access to the "id-token" permission' ) ,
{ code : 'EUSAGE' }
)
}
} else if ( ciInfo . GITLAB ) {
// Ensure that the Sigstore OIDC token is available
if ( ! process . env . SIGSTORE _ID _TOKEN ) {
throw Object . assign (
/* eslint-disable-next-line max-len */
new Error ( 'Provenance generation in GitLab CI requires "SIGSTORE_ID_TOKEN" with "sigstore" audience to be present in "id_tokens". For more info see:\nhttps://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html' ) ,
{ code : 'EUSAGE' }
)
}
} else {
2023-06-08 05:24:49 -07:00
throw Object . assign (
2023-06-22 07:48:43 -07:00
new Error ( 'Automatic provenance generation not supported for provider: ' + ciInfo . name ) ,
2023-06-08 05:24:49 -07:00
{ code : 'EUSAGE' }
)
}
// Some registries (e.g. GH packages) require auth to check visibility,
// and always return 404 when no auth is supplied. In this case we assume
// the package is always private and require `--access public` to publish
// with provenance.
let visibility = { public : false }
2023-06-22 07:48:43 -07:00
if ( opts . access !== 'public' ) {
2023-06-08 05:24:49 -07:00
try {
const res = await npmFetch
. json ( ` ${ registry } /-/package/ ${ spec . escapedName } /visibility ` , opts )
visibility = res
} catch ( err ) {
if ( err . code !== 'E404' ) {
throw err
}
}
}
if ( ! visibility . public && opts . provenance === true && opts . access !== 'public' ) {
throw Object . assign (
/* eslint-disable-next-line max-len */
new Error ( "Can't generate provenance for new or private package, you must set `access` to public." ) ,
{ code : 'EUSAGE' }
)
}
}
2020-11-03 20:39:24 -05:00
module . exports = publish