Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
publish.js227 linesDownload Raw Back to lib
1const npmFetch = require('npm-registry-fetch')
2const npa = require('npm-package-arg')
3const PackageJson = require('@npmcli/package-json')
4const { log } = require('proc-log')
5const semver = require('semver')
6const { URL } = require('node:url')
7const ssri = require('ssri')
8const ciInfo = require('ci-info')
9
10const { generateProvenance, verifyProvenance } = require('./provenance')
11
12const TLOG_BASE_URL = 'https://search.sigstore.dev/'
13
14const publish = async (manifest, tarballData, opts) => {
15  if (manifest.private) {
16    throw Object.assign(
17      new Error(`This package has been marked as private
18Remove the 'private' field from the package.json to publish it.`),
19      { code: 'EPRIVATE' }
20    )
21  }
22
23  // spec is used to pick the appropriate registry/auth combo
24  const spec = npa.resolve(manifest.name, manifest.version)
25  opts = {
26    access: 'public',
27    algorithms: ['sha512'],
28    defaultTag: 'latest',
29    ...opts,
30    spec,
31  }
32
33  const reg = npmFetch.pickRegistry(spec, opts)
34  const pubManifest = await patchManifest(manifest, opts)
35
36  // registry-frontdoor cares about the access level,
37  // which is only configurable for scoped packages
38  if (!spec.scope && opts.access === 'restricted') {
39    throw Object.assign(
40      new Error("Can't restrict access to unscoped packages."),
41      { code: 'EUNSCOPED' }
42    )
43  }
44
45  const { metadata, transparencyLogUrl } = await buildMetadata(
46    reg,
47    pubManifest,
48    tarballData,
49    spec,
50    opts
51  )
52
53  const res = await npmFetch(spec.escapedName, {
54    ...opts,
55    method: 'PUT',
56    body: metadata,
57    ignoreBody: true,
58  })
59  if (transparencyLogUrl) {
60    res.transparencyLogUrl = transparencyLogUrl
61  }
62  return res
63}
64
65const patchManifest = async (_manifest, opts) => {
66  const { npmVersion } = opts
67  const steps = ['fixName']
68  const manifestInput = { ..._manifest, _nodeVersion: process.versions.node }
69  if (npmVersion != null) {
70    manifestInput._npmVersion = npmVersion
71  }
72  const manifest = await new PackageJson()
73    .fromContent(manifestInput)
74    .normalize({ steps })
75    .then(p => p.content)
76
77  const version = semver.clean(manifest.version)
78  if (!version) {
79    throw Object.assign(
80      new Error('invalid semver: ' + manifest.version),
81      { code: 'EBADSEMVER' }
82    )
83  }
84  manifest.version = version
85  return manifest
86}
87
88const buildMetadata = async (registry, manifest, tarballData, spec, opts) => {
89  const { access, defaultTag, algorithms, provenance, provenanceFile } = opts
90  const root = {
91    _id: manifest.name,
92    name: manifest.name,
93    description: manifest.description,
94    'dist-tags': {},
95    versions: {},
96    access,
97  }
98
99  root.versions[manifest.version] = manifest
100  const tag = manifest.tag || defaultTag
101  root['dist-tags'][tag] = manifest.version
102
103  const tarballName = `${manifest.name}-${manifest.version}.tgz`
104  const provenanceBundleName = `${manifest.name}-${manifest.version}.sigstore`
105  const tarballURI = `${manifest.name}/-/${tarballName}`
106  const integrity = ssri.fromData(tarballData, {
107    algorithms: [...new Set(['sha1'].concat(algorithms))],
108  })
109
110  manifest._id = `${manifest.name}@${manifest.version}`
111  manifest.dist = { ...manifest.dist }
112  // Don't bother having sha1 in the actual integrity field
113  manifest.dist.integrity = integrity.sha512[0].toString()
114  // Legacy shasum support
115  manifest.dist.shasum = integrity.sha1[0].hexDigest()
116
117  // NB: the CLI always fetches via HTTPS if the registry is HTTPS,
118  // regardless of what's here.  This makes it so that installing
119  // from an HTTP-only mirror doesn't cause problems, though.
120  manifest.dist.tarball = new URL(tarballURI, registry).href
121    .replace(/^https:\/\//, 'http://')
122
123  root._attachments = {}
124  root._attachments[tarballName] = {
125    content_type: 'application/octet-stream',
126    data: tarballData.toString('base64'),
127    length: tarballData.length,
128  }
129
130  // Handle case where --provenance flag was set to true
131  let transparencyLogUrl
132  if (provenance === true || provenanceFile) {
133    let provenanceBundle
134    const subject = {
135      name: npa.toPurl(spec),
136      digest: { sha512: integrity.sha512[0].hexDigest() },
137    }
138
139    if (provenance === true) {
140      await ensureProvenanceGeneration(registry, spec, opts)
141      provenanceBundle = await generateProvenance([subject], opts)
142
143      /* eslint-disable-next-line max-len */
144      log.notice('publish', `Signed provenance statement with source and build information from ${ciInfo.name}`)
145
146      const tlogEntry = provenanceBundle?.verificationMaterial?.tlogEntries[0]
147      /* istanbul ignore else */
148      if (tlogEntry) {
149        transparencyLogUrl = `${TLOG_BASE_URL}?logIndex=${tlogEntry.logIndex}`
150        log.notice(
151          'publish',
152          `Provenance statement published to transparency log: ${transparencyLogUrl}`
153        )
154      }
155    } else {
156      provenanceBundle = await verifyProvenance(subject, provenanceFile)
157    }
158
159    const serializedBundle = JSON.stringify(provenanceBundle)
160    root._attachments[provenanceBundleName] = {
161      content_type: provenanceBundle.mediaType,
162      data: serializedBundle,
163      length: serializedBundle.length,
164    }
165  }
166
167  return {
168    metadata: root,
169    transparencyLogUrl,
170  }
171}
172
173// Check that all the prereqs are met for provenance generation
174const ensureProvenanceGeneration = async (registry, spec, opts) => {
175  if (ciInfo.GITHUB_ACTIONS) {
176    // Ensure that the GHA OIDC token is available
177    if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
178      throw Object.assign(
179        /* eslint-disable-next-line max-len */
180        new Error('Provenance generation in GitHub Actions requires "write" access to the "id-token" permission'),
181        { code: 'EUSAGE' }
182      )
183    }
184  } else if (ciInfo.GITLAB) {
185    // Ensure that the Sigstore OIDC token is available
186    if (!process.env.SIGSTORE_ID_TOKEN) {
187      throw Object.assign(
188        /* eslint-disable-next-line max-len */
189        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'),
190        { code: 'EUSAGE' }
191      )
192    }
193  } else {
194    throw Object.assign(
195      new Error('Automatic provenance generation not supported for provider: ' + ciInfo.name),
196      { code: 'EUSAGE' }
197    )
198  }
199
200  // Some registries (e.g. GH packages) require auth to check visibility,
201  // and always return 404 when no auth is supplied. In this case we assume
202  // the package is always private and require `--access public` to publish
203  // with provenance.
204  let visibility = { public: false }
205  if (opts.access !== 'public') {
206    try {
207      const res = await npmFetch
208        .json(`/-/package/${spec.escapedName}/visibility`, { ...opts, registry })
209      visibility = res
210    } catch (err) {
211      if (err.code !== 'E404') {
212        throw err
213      }
214    }
215  }
216
217  if (!visibility.public && opts.provenance === true && opts.access !== 'public') {
218    throw Object.assign(
219      /* eslint-disable-next-line max-len */
220      new Error("Can't generate provenance for new or private package, you must set `access` to public."),
221      { code: 'EUSAGE' }
222    )
223  }
224}
225
226module.exports = publish
227 
codekingpro/portable-devtools · Team Ai