From 7b2ab6d13c022b9bfec882df499d57597a38d691 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Sun, 16 Aug 2026 13:24:18 +0900 Subject: [PATCH 1/5] fix: provenance-file takes precedence over OIDC auto-generated provenance When publishing with an externally generated provenance bundle under OIDC trusted publishing, the auto-enable logic in the OIDC flow unconditionally set `provenance: true`, which caused libnpmpublish to silently discard the supplied `--provenance-file` bundle and publish its own generated provenance instead. Skip auto-enabling provenance when a provenance file is configured, so the explicitly supplied bundle is verified and published as documented. Fixes: https://github.com/npm/cli/issues/9879 Signed-off-by: Yunseo Kim --- lib/utils/oidc.js | 5 +- test/fixtures/mock-oidc.js | 4 +- test/lib/commands/publish.js | 119 +++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/lib/utils/oidc.js b/lib/utils/oidc.js index 00f32c642621c..dd41e96c92739 100644 --- a/lib/utils/oidc.js +++ b/lib/utils/oidc.js @@ -143,8 +143,9 @@ async function oidc ({ packageName, registry, opts, config }) { try { const isDefaultProvenance = config.isDefault('provenance') - // CircleCI doesn't support provenance yet, so skip the auto-enable logic - if (isDefaultProvenance && !ciInfo.CIRCLE) { + // CircleCI doesn't support provenance yet, so skip the auto-enable logic. + // An explicitly provided provenance file always takes precedence over auto-generated provenance + if (isDefaultProvenance && !ciInfo.CIRCLE && !opts.provenanceFile) { const [headerB64, payloadB64] = idToken.split('.') if (headerB64 && payloadB64) { const payloadJson = Buffer.from(payloadB64, 'base64').toString('utf8') diff --git a/test/fixtures/mock-oidc.js b/test/fixtures/mock-oidc.js index d15d52c1b819f..a9581765ff2c4 100644 --- a/test/fixtures/mock-oidc.js +++ b/test/fixtures/mock-oidc.js @@ -101,7 +101,7 @@ const mockOidc = async (t, { ciInfo.CIRCLE = CIRCLE }) - const { npm, registry, joinedOutput, logs } = await loadNpmWithRegistry(t, { + const { npm, registry, joinedOutput, logs, prefix } = await loadNpmWithRegistry(t, { config: { loglevel: 'silly', ...config, @@ -160,7 +160,7 @@ const mockOidc = async (t, { }) } - return { npm, joinedOutput, logs, ACTIONS_ID_TOKEN_REQUEST_URL } + return { npm, registry, prefix, joinedOutput, logs, ACTIONS_ID_TOKEN_REQUEST_URL } } const oidcPublishTest = (opts) => { diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index fe286ff46b748..c56557bee4053 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -3,6 +3,8 @@ const { loadNpmWithRegistry } = require('../../fixtures/mock-npm') const { cleanZlib } = require('../../fixtures/clean-snapshot') const pacote = require('pacote') const Arborist = require('@npmcli/arborist') +const npa = require('npm-package-arg') +const ssri = require('ssri') const path = require('node:path') const fs = require('node:fs') const { circleciIdToken, githubIdToken, gitlabIdToken, oidcPublishTest, mockOidc } = require('../../fixtures/mock-oidc') @@ -1533,6 +1535,123 @@ t.test('oidc token exchange - provenance', (t) => { }, })) + t.test('provenance-file config should take precedence over OIDC auto-provenance', async t => { + const bundleDir = t.testdir() + const provenanceBundlePath = path.join(bundleDir, 'provenance-bundle.json') + // holder so the libnpmpack mock can return the tarball computed below + const packMock = { tarballData: null } + + const { npm, registry, prefix, joinedOutput } = await mockOidc(t, { + oidcOptions: { github: true }, + config: { + '//registry.npmjs.org/:_authToken': 'existing-fallback-token', + 'provenance-file': provenanceBundlePath, + }, + mockGithubOidcOptions: { + audience: 'npm:registry.npmjs.org', + idToken: githubPublicIdToken, + }, + mockOidcTokenExchangeOptions: { + idToken: githubPublicIdToken, + body: { + token: 'exchange-token', + }, + }, + publishOptions: { + token: 'exchange-token', + noPut: true, + }, + load: { + mocks: { + // the registry visibility check passes for this public package so the + // OIDC flow would auto-enable provenance unless provenance-file wins + libnpmaccess: { + getVisibility: async () => ({ public: true }), + }, + // publish a deterministic tarball so the bundle subject digest can match it + libnpmpack: async () => packMock.tarballData, + // libnpmpublish must be mocked as a module so its internal require of + // sigstore is intercepted: a user-supplied bundle is only verified, + // generation (attest) must never run + libnpmpublish: t.mock('libnpmpublish', { + 'libnpmpublish/lib/provenance': t.mock('libnpmpublish/lib/provenance', { + sigstore: { + verify: async () => {}, + attest: async () => { + throw new Error('sigstore.attest must not be called when provenance-file is configured') + }, + }, + }), + }), + }, + }, + }) + + // compute the tarball integrity the same way libnpmpublish does so the + // provenance bundle subject matches the packed tarball + packMock.tarballData = await pacote.tarball(prefix, { Arborist }) + const integrity = ssri.fromData(packMock.tarballData, { algorithms: ['sha512'] }) + const spec = npa.resolve(pkg, '1.0.0') + const provenanceBundle = { + mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.2', + verificationMaterial: { + x509CertificateChain: { + certificates: [{ rawBytes: 'dGVzdA==' }], + }, + tlogEntries: [], + }, + dsseEnvelope: { + payload: Buffer.from(JSON.stringify({ + _type: 'https://in-toto.io/Statement/v0.1', + subject: [ + { + name: npa.toPurl(spec), + digest: { sha512: integrity.sha512[0].hexDigest() }, + }, + ], + predicateType: 'https://slsa.dev/provenance/v0.2', + predicate: {}, + })).toString('base64'), + payloadType: 'application/vnd.in-toto+json', + signatures: [{ + /* eslint-disable-next-line max-len */ + sig: 'MEUCIQDqHtpkk1d0rMGLmf3qet9jLale3KVn8Pnywpwt7ln+9AIgG9CJvvUmyemhNYHz0DfJ4vMfKk1TMg+m3hR0mISXJos=', + keyid: '', + }], + }, + } + fs.writeFileSync(provenanceBundlePath, JSON.stringify(provenanceBundle, null, 2)) + + let publishedBody + registry.nock + .put(`/${spec.escapedName}`, (body) => { + publishedBody = body + return true + }) + .matchHeader('authorization', 'Bearer exchange-token') + // optional so a failed publish does not leave a pending mock behind + .optionally() + .reply(200, {}) + + // libnpmpublish checks package visibility itself before generating + // provenance; optional so it is only consumed if generation is attempted + registry.nock + .get(`/-/package/${spec.escapedName}/visibility`) + .optionally() + .reply(200, { public: true }) + + await npm.exec('publish', []) + + t.match(joinedOutput(), '+ @npmcli/test-package@1.0.0') + const sigstoreAttachment = publishedBody?._attachments[`${pkg}-1.0.0.sigstore`] + t.ok(sigstoreAttachment, 'published packument includes a sigstore bundle attachment') + t.strictSame( + JSON.parse(sigstoreAttachment.data), + provenanceBundle, + 'published sigstore bundle is the user-supplied provenance file' + ) + }) + const brokenJwts = [ 'x.invalid-jwt.x', 'x.invalid-jwt.', From 66710dcf8f693bb70e04834017734b2ecd674e24 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Sun, 16 Aug 2026 13:24:27 +0900 Subject: [PATCH 2/5] fix(libnpmpublish): throw when provenance and provenanceFile are both set The two options are documented as mutually exclusive, but buildMetadata previously preferred generation when both were set, silently discarding the supplied bundle. Throw an EPROVENANCECONFLICT usage error instead of silently discarding a caller-supplied cryptographically meaningful artifact. Signed-off-by: Yunseo Kim --- workspaces/libnpmpublish/README.md | 8 +++-- workspaces/libnpmpublish/lib/publish.js | 6 ++++ workspaces/libnpmpublish/test/publish.js | 42 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/workspaces/libnpmpublish/README.md b/workspaces/libnpmpublish/README.md index 69b747d92dcfc..a21140ead0031 100644 --- a/workspaces/libnpmpublish/README.md +++ b/workspaces/libnpmpublish/README.md @@ -58,11 +58,15 @@ A couple of options of note: * `opts.provenance` - when running in a supported CI environment, will trigger the generation of a signed provenance statement to be published alongside - the package. Mutually exclusive with the `provenanceFile` option. + the package. Mutually exclusive with the `provenanceFile` option; providing + both will throw an `EPROVENANCECONFLICT` error. In the npm CLI's trusted + publishing flows, automatic provenance generation is skipped when + `provenanceFile` is supplied. * `opts.provenanceFile` - specifies the path to an externally-generated provenance statement to be published alongside the package. Mutually - exclusive with the `provenance` option. The specified file should be a + exclusive with the `provenance` option; providing both will throw an + `EPROVENANCECONFLICT` error. The specified file should be a [Sigstore Bundle](https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto) containing a [DSSE](https://github.com/secure-systems-lab/dsse)-packaged provenance statement. diff --git a/workspaces/libnpmpublish/lib/publish.js b/workspaces/libnpmpublish/lib/publish.js index 796cef6aa392a..33a46d56c26d5 100644 --- a/workspaces/libnpmpublish/lib/publish.js +++ b/workspaces/libnpmpublish/lib/publish.js @@ -144,6 +144,12 @@ const buildMetadata = async (registry, manifest, tarballData, spec, opts) => { // Handle case where --provenance flag was set to true let transparencyLogUrl + if (provenance === true && provenanceFile) { + throw Object.assign( + new Error('provenance and provenanceFile are mutually exclusive'), + { code: 'EPROVENANCECONFLICT' } + ) + } if (provenance === true || provenanceFile) { let provenanceBundle const subject = { diff --git a/workspaces/libnpmpublish/test/publish.js b/workspaces/libnpmpublish/test/publish.js index 3b41656bef17a..6d5dd0ed569e0 100644 --- a/workspaces/libnpmpublish/test/publish.js +++ b/workspaces/libnpmpublish/test/publish.js @@ -862,6 +862,48 @@ t.test('user-supplied provenance - success', async t => { t.ok(ret, 'publish succeeded') }) +t.test('provenance and provenanceFile together throws', async t => { + mockGlobals(t, { + 'process.env': { + CI: true, + GITHUB_ACTIONS: true, + ACTIONS_ID_TOKEN_REQUEST_URL: 'https://mock.oidc', + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'decafbad', + }, + }) + + const { publish } = t.mock('..', { + 'ci-info': { GITHUB_ACTIONS: true, name: 'GitHub Actions' }, + '../lib/provenance': { + generateProvenance: () => { + throw new Error('generateProvenance should not be called') + }, + verifyProvenance: () => { + throw new Error('verifyProvenance should not be called') + }, + }, + }) + + const manifest = { + name: '@npmcli/libnpmpublish-test', + version: '1.0.0', + description: 'test libnpmpublish package', + } + + await t.rejects( + publish(manifest, tarData, { + ...opts, + access: 'public', + provenance: true, + provenanceFile: './test/fixtures/valid-bundle.json', + }), + { + code: 'EPROVENANCECONFLICT', + message: /mutually exclusive/, + } + ) +}) + t.test('user-supplied provenance - failure', async t => { const { publish } = t.mock('..') const manifest = { From df4e3001e5cab941764e714c70f7fcaccc5e28b8 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Sun, 16 Aug 2026 13:24:34 +0900 Subject: [PATCH 3/5] docs: document provenance-file precedence over automatic provenance Signed-off-by: Yunseo Kim --- tap-snapshots/test/lib/docs.js.test.cjs | 6 ++++++ workspaces/config/lib/definitions/definitions.js | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index dbfc88ab63abc..9c656e35b245a 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -1750,6 +1750,9 @@ Set to \`false\` to suppress the progress bar. When publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from. +When the \`provenance-file\` config is set, it takes precedence and automatic +provenance generation (including via trusted publishing/OIDC) is skipped. + This config cannot be used with: \`provenance-file\` #### \`provenance-file\` @@ -1759,6 +1762,9 @@ This config cannot be used with: \`provenance-file\` When publishing, the provenance bundle at the given path will be used. +This takes precedence over automatic provenance generation in trusted +publishing flows. + This config cannot be used with: \`provenance\` #### \`proxy\` diff --git a/workspaces/config/lib/definitions/definitions.js b/workspaces/config/lib/definitions/definitions.js index f932d8f48103c..8ada20fcad6ee 100644 --- a/workspaces/config/lib/definitions/definitions.js +++ b/workspaces/config/lib/definitions/definitions.js @@ -2006,6 +2006,10 @@ const definitions = { description: ` When publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from. + + When the \`provenance-file\` config is set, it takes precedence and + automatic provenance generation (including via trusted publishing/OIDC) + is skipped. `, flatten, }), @@ -2016,6 +2020,9 @@ const definitions = { exclusive: ['provenance'], description: ` When publishing, the provenance bundle at the given path will be used. + + This takes precedence over automatic provenance generation in trusted + publishing flows. `, flatten, }), From 71f3f4855b2a73d7b5be307242112c7cec8cb450 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Sat, 29 Aug 2026 02:47:06 +0900 Subject: [PATCH 4/5] fix(libnpmpublish): use EUSAGE for provenance and provenanceFile conflict Per review on #9882, reuse the existing EUSAGE convention instead of introducing a dedicated EPROVENANCECONFLICT error code, and align the message and README wording accordingly. Signed-off-by: Yunseo Kim --- workspaces/libnpmpublish/README.md | 4 ++-- workspaces/libnpmpublish/lib/publish.js | 4 ++-- workspaces/libnpmpublish/test/publish.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/workspaces/libnpmpublish/README.md b/workspaces/libnpmpublish/README.md index a21140ead0031..897c61b7bfe82 100644 --- a/workspaces/libnpmpublish/README.md +++ b/workspaces/libnpmpublish/README.md @@ -59,14 +59,14 @@ A couple of options of note: * `opts.provenance` - when running in a supported CI environment, will trigger the generation of a signed provenance statement to be published alongside the package. Mutually exclusive with the `provenanceFile` option; providing - both will throw an `EPROVENANCECONFLICT` error. In the npm CLI's trusted + both will throw an `EUSAGE` error. In the npm CLI's trusted publishing flows, automatic provenance generation is skipped when `provenanceFile` is supplied. * `opts.provenanceFile` - specifies the path to an externally-generated provenance statement to be published alongside the package. Mutually exclusive with the `provenance` option; providing both will throw an - `EPROVENANCECONFLICT` error. The specified file should be a + `EUSAGE` error. The specified file should be a [Sigstore Bundle](https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto) containing a [DSSE](https://github.com/secure-systems-lab/dsse)-packaged provenance statement. diff --git a/workspaces/libnpmpublish/lib/publish.js b/workspaces/libnpmpublish/lib/publish.js index 33a46d56c26d5..d9afc066eb4ba 100644 --- a/workspaces/libnpmpublish/lib/publish.js +++ b/workspaces/libnpmpublish/lib/publish.js @@ -146,8 +146,8 @@ const buildMetadata = async (registry, manifest, tarballData, spec, opts) => { let transparencyLogUrl if (provenance === true && provenanceFile) { throw Object.assign( - new Error('provenance and provenanceFile are mutually exclusive'), - { code: 'EPROVENANCECONFLICT' } + new Error('provenance and provenanceFile cannot be used together'), + { code: 'EUSAGE' } ) } if (provenance === true || provenanceFile) { diff --git a/workspaces/libnpmpublish/test/publish.js b/workspaces/libnpmpublish/test/publish.js index 6d5dd0ed569e0..f1e6b28c6ae19 100644 --- a/workspaces/libnpmpublish/test/publish.js +++ b/workspaces/libnpmpublish/test/publish.js @@ -898,8 +898,8 @@ t.test('provenance and provenanceFile together throws', async t => { provenanceFile: './test/fixtures/valid-bundle.json', }), { - code: 'EPROVENANCECONFLICT', - message: /mutually exclusive/, + code: 'EUSAGE', + message: /cannot be used together/, } ) }) From 6907480d128cac9d99498d66650af07b9d34b121 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Sat, 29 Aug 2026 03:01:04 +0900 Subject: [PATCH 5/5] test: parameterize provenance-file precedence regression test Cover both config sources for the provenance-file precedence regression without duplicating the test body: the CLI config case exercises the existing path through the config layer, and the publishConfig case exercises Publish.#getManifest() and its flatten(filteredPublishConfig, opts) call, which runs before oidc() decides whether to auto-enable provenance. The publishConfig case proves the value reaches opts.provenanceFile, the OIDC token exchange still occurs, automatic generation stays off, and the supplied bundle is verified and published byte-for-byte with sigstore.attest() never called. Signed-off-by: Yunseo Kim --- test/lib/commands/publish.js | 243 ++++++++++++++++++++--------------- 1 file changed, 138 insertions(+), 105 deletions(-) diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index c56557bee4053..b6282a86fbe1c 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -1535,122 +1535,155 @@ t.test('oidc token exchange - provenance', (t) => { }, })) - t.test('provenance-file config should take precedence over OIDC auto-provenance', async t => { - const bundleDir = t.testdir() - const provenanceBundlePath = path.join(bundleDir, 'provenance-bundle.json') - // holder so the libnpmpack mock can return the tarball computed below - const packMock = { tarballData: null } + const provenanceFileSources = [ + { + name: 'CLI config', + options: provenanceBundlePath => ({ + config: { + 'provenance-file': provenanceBundlePath, + }, + }), + }, + { + // exercises Publish.#getManifest() and its flatten(filteredPublishConfig, opts) + // path: publishConfig must reach opts.provenanceFile before oidc() decides + // whether to enable automatic provenance + name: 'publishConfig', + options: provenanceBundlePath => ({ + packageJson: { + publishConfig: { + 'provenance-file': provenanceBundlePath, + }, + }, + }), + }, + ] - const { npm, registry, prefix, joinedOutput } = await mockOidc(t, { - oidcOptions: { github: true }, - config: { - '//registry.npmjs.org/:_authToken': 'existing-fallback-token', - 'provenance-file': provenanceBundlePath, - }, - mockGithubOidcOptions: { - audience: 'npm:registry.npmjs.org', - idToken: githubPublicIdToken, - }, - mockOidcTokenExchangeOptions: { - idToken: githubPublicIdToken, - body: { - token: 'exchange-token', + for (const { name, options } of provenanceFileSources) { + t.test(`${name} provenance-file takes precedence over OIDC auto-provenance`, async t => { + const bundleDir = t.testdir() + const provenanceBundlePath = path.join( + bundleDir, + 'provenance-bundle.json' + ) + // holder so the libnpmpack mock can return the tarball computed below + const packMock = { tarballData: null } + + const sourceOptions = options(provenanceBundlePath) + + const { npm, registry, prefix, joinedOutput } = await mockOidc(t, { + oidcOptions: { github: true }, + config: { + '//registry.npmjs.org/:_authToken': 'existing-fallback-token', + ...sourceOptions.config, }, - }, - publishOptions: { - token: 'exchange-token', - noPut: true, - }, - load: { - mocks: { - // the registry visibility check passes for this public package so the - // OIDC flow would auto-enable provenance unless provenance-file wins - libnpmaccess: { - getVisibility: async () => ({ public: true }), + packageJson: sourceOptions.packageJson, + mockGithubOidcOptions: { + audience: 'npm:registry.npmjs.org', + idToken: githubPublicIdToken, + }, + mockOidcTokenExchangeOptions: { + idToken: githubPublicIdToken, + body: { + token: 'exchange-token', }, - // publish a deterministic tarball so the bundle subject digest can match it - libnpmpack: async () => packMock.tarballData, - // libnpmpublish must be mocked as a module so its internal require of - // sigstore is intercepted: a user-supplied bundle is only verified, - // generation (attest) must never run - libnpmpublish: t.mock('libnpmpublish', { - 'libnpmpublish/lib/provenance': t.mock('libnpmpublish/lib/provenance', { - sigstore: { - verify: async () => {}, - attest: async () => { - throw new Error('sigstore.attest must not be called when provenance-file is configured') + }, + publishOptions: { + token: 'exchange-token', + noPut: true, + }, + load: { + mocks: { + libnpmaccess: { + getVisibility: async () => ({ public: true }), + }, + // publish a deterministic tarball so the bundle subject digest can match it + libnpmpack: async () => packMock.tarballData, + // libnpmpublish must be mocked as a module so its internal require of + // sigstore is intercepted: a user-supplied bundle is only verified, + // generation (attest) must never run + libnpmpublish: t.mock('libnpmpublish', { + 'libnpmpublish/lib/provenance': t.mock('libnpmpublish/lib/provenance', { + sigstore: { + verify: async () => {}, + attest: async () => { + throw new Error('sigstore.attest must not be called when provenance-file is configured') + }, }, - }, + }), }), - }), + }, }, - }, - }) + }) - // compute the tarball integrity the same way libnpmpublish does so the - // provenance bundle subject matches the packed tarball - packMock.tarballData = await pacote.tarball(prefix, { Arborist }) - const integrity = ssri.fromData(packMock.tarballData, { algorithms: ['sha512'] }) - const spec = npa.resolve(pkg, '1.0.0') - const provenanceBundle = { - mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.2', - verificationMaterial: { - x509CertificateChain: { - certificates: [{ rawBytes: 'dGVzdA==' }], + // compute the tarball integrity the same way libnpmpublish does so the + // provenance bundle subject matches the packed tarball + packMock.tarballData = await pacote.tarball(prefix, { Arborist }) + const integrity = ssri.fromData(packMock.tarballData, { algorithms: ['sha512'] }) + const spec = npa.resolve(pkg, '1.0.0') + const provenanceBundle = { + mediaType: 'application/vnd.dev.sigstore.bundle+json;version=0.2', + verificationMaterial: { + x509CertificateChain: { + certificates: [{ rawBytes: 'dGVzdA==' }], + }, + tlogEntries: [], }, - tlogEntries: [], - }, - dsseEnvelope: { - payload: Buffer.from(JSON.stringify({ - _type: 'https://in-toto.io/Statement/v0.1', - subject: [ - { - name: npa.toPurl(spec), - digest: { sha512: integrity.sha512[0].hexDigest() }, - }, - ], - predicateType: 'https://slsa.dev/provenance/v0.2', - predicate: {}, - })).toString('base64'), - payloadType: 'application/vnd.in-toto+json', - signatures: [{ - /* eslint-disable-next-line max-len */ - sig: 'MEUCIQDqHtpkk1d0rMGLmf3qet9jLale3KVn8Pnywpwt7ln+9AIgG9CJvvUmyemhNYHz0DfJ4vMfKk1TMg+m3hR0mISXJos=', - keyid: '', - }], - }, - } - fs.writeFileSync(provenanceBundlePath, JSON.stringify(provenanceBundle, null, 2)) + dsseEnvelope: { + payload: Buffer.from(JSON.stringify({ + _type: 'https://in-toto.io/Statement/v0.1', + subject: [ + { + name: npa.toPurl(spec), + digest: { sha512: integrity.sha512[0].hexDigest() }, + }, + ], + predicateType: 'https://slsa.dev/provenance/v0.2', + predicate: {}, + })).toString('base64'), + payloadType: 'application/vnd.in-toto+json', + signatures: [{ + /* eslint-disable-next-line max-len */ + sig: 'MEUCIQDqHtpkk1d0rMGLmf3qet9jLale3KVn8Pnywpwt7ln+9AIgG9CJvvUmyemhNYHz0DfJ4vMfKk1TMg+m3hR0mISXJos=', + keyid: '', + }], + }, + } + fs.writeFileSync(provenanceBundlePath, JSON.stringify(provenanceBundle, null, 2)) + + let publishedBody + registry.nock + .put(`/${spec.escapedName}`, (body) => { + publishedBody = body + return true + }) + .matchHeader('authorization', 'Bearer exchange-token') + // optional so a failed publish does not leave a pending mock behind + .optionally() + .reply(200, {}) + + // libnpmpublish checks package visibility itself before generating + // provenance; optional so it is only consumed if generation is attempted + registry.nock + .get(`/-/package/${spec.escapedName}/visibility`) + .optionally() + .reply(200, { public: true }) - let publishedBody - registry.nock - .put(`/${spec.escapedName}`, (body) => { - publishedBody = body - return true - }) - .matchHeader('authorization', 'Bearer exchange-token') - // optional so a failed publish does not leave a pending mock behind - .optionally() - .reply(200, {}) - - // libnpmpublish checks package visibility itself before generating - // provenance; optional so it is only consumed if generation is attempted - registry.nock - .get(`/-/package/${spec.escapedName}/visibility`) - .optionally() - .reply(200, { public: true }) + await npm.exec('publish', []) - await npm.exec('publish', []) + t.match(joinedOutput(), '+ @npmcli/test-package@1.0.0') - t.match(joinedOutput(), '+ @npmcli/test-package@1.0.0') - const sigstoreAttachment = publishedBody?._attachments[`${pkg}-1.0.0.sigstore`] - t.ok(sigstoreAttachment, 'published packument includes a sigstore bundle attachment') - t.strictSame( - JSON.parse(sigstoreAttachment.data), - provenanceBundle, - 'published sigstore bundle is the user-supplied provenance file' - ) - }) + const attachment = + publishedBody?._attachments[`${pkg}-1.0.0.sigstore`] + + t.ok(attachment, 'published packument includes supplied provenance') + t.strictSame( + JSON.parse(attachment.data), + provenanceBundle, + 'published sigstore bundle is the user-supplied provenance file' + ) + }) + } const brokenJwts = [ 'x.invalid-jwt.x',