@@ -1535,122 +1535,155 @@ t.test('oidc token exchange - provenance', (t) => {
15351535 } ,
15361536 } ) )
15371537
1538- t . test ( 'provenance-file config should take precedence over OIDC auto-provenance' , async t => {
1539- const bundleDir = t . testdir ( )
1540- const provenanceBundlePath = path . join ( bundleDir , 'provenance-bundle.json' )
1541- // holder so the libnpmpack mock can return the tarball computed below
1542- const packMock = { tarballData : null }
1538+ const provenanceFileSources = [
1539+ {
1540+ name : 'CLI config' ,
1541+ options : provenanceBundlePath => ( {
1542+ config : {
1543+ 'provenance-file' : provenanceBundlePath ,
1544+ } ,
1545+ } ) ,
1546+ } ,
1547+ {
1548+ // exercises Publish.#getManifest() and its flatten(filteredPublishConfig, opts)
1549+ // path: publishConfig must reach opts.provenanceFile before oidc() decides
1550+ // whether to enable automatic provenance
1551+ name : 'publishConfig' ,
1552+ options : provenanceBundlePath => ( {
1553+ packageJson : {
1554+ publishConfig : {
1555+ 'provenance-file' : provenanceBundlePath ,
1556+ } ,
1557+ } ,
1558+ } ) ,
1559+ } ,
1560+ ]
15431561
1544- const { npm, registry, prefix, joinedOutput } = await mockOidc ( t , {
1545- oidcOptions : { github : true } ,
1546- config : {
1547- '//registry.npmjs.org/:_authToken' : 'existing-fallback-token' ,
1548- 'provenance-file' : provenanceBundlePath ,
1549- } ,
1550- mockGithubOidcOptions : {
1551- audience : 'npm:registry.npmjs.org' ,
1552- idToken : githubPublicIdToken ,
1553- } ,
1554- mockOidcTokenExchangeOptions : {
1555- idToken : githubPublicIdToken ,
1556- body : {
1557- token : 'exchange-token' ,
1562+ for ( const { name, options } of provenanceFileSources ) {
1563+ t . test ( `${ name } provenance-file takes precedence over OIDC auto-provenance` , async t => {
1564+ const bundleDir = t . testdir ( )
1565+ const provenanceBundlePath = path . join (
1566+ bundleDir ,
1567+ 'provenance-bundle.json'
1568+ )
1569+ // holder so the libnpmpack mock can return the tarball computed below
1570+ const packMock = { tarballData : null }
1571+
1572+ const sourceOptions = options ( provenanceBundlePath )
1573+
1574+ const { npm, registry, prefix, joinedOutput } = await mockOidc ( t , {
1575+ oidcOptions : { github : true } ,
1576+ config : {
1577+ '//registry.npmjs.org/:_authToken' : 'existing-fallback-token' ,
1578+ ...sourceOptions . config ,
15581579 } ,
1559- } ,
1560- publishOptions : {
1561- token : 'exchange-token' ,
1562- noPut : true ,
1563- } ,
1564- load : {
1565- mocks : {
1566- // the registry visibility check passes for this public package so the
1567- // OIDC flow would auto-enable provenance unless provenance-file wins
1568- libnpmaccess : {
1569- getVisibility : async ( ) => ( { public : true } ) ,
1580+ packageJson : sourceOptions . packageJson ,
1581+ mockGithubOidcOptions : {
1582+ audience : 'npm:registry.npmjs.org' ,
1583+ idToken : githubPublicIdToken ,
1584+ } ,
1585+ mockOidcTokenExchangeOptions : {
1586+ idToken : githubPublicIdToken ,
1587+ body : {
1588+ token : 'exchange-token' ,
15701589 } ,
1571- // publish a deterministic tarball so the bundle subject digest can match it
1572- libnpmpack : async ( ) => packMock . tarballData ,
1573- // libnpmpublish must be mocked as a module so its internal require of
1574- // sigstore is intercepted: a user-supplied bundle is only verified,
1575- // generation (attest) must never run
1576- libnpmpublish : t . mock ( 'libnpmpublish' , {
1577- 'libnpmpublish/lib/provenance' : t . mock ( 'libnpmpublish/lib/provenance' , {
1578- sigstore : {
1579- verify : async ( ) => { } ,
1580- attest : async ( ) => {
1581- throw new Error ( 'sigstore.attest must not be called when provenance-file is configured' )
1590+ } ,
1591+ publishOptions : {
1592+ token : 'exchange-token' ,
1593+ noPut : true ,
1594+ } ,
1595+ load : {
1596+ mocks : {
1597+ libnpmaccess : {
1598+ getVisibility : async ( ) => ( { public : true } ) ,
1599+ } ,
1600+ // publish a deterministic tarball so the bundle subject digest can match it
1601+ libnpmpack : async ( ) => packMock . tarballData ,
1602+ // libnpmpublish must be mocked as a module so its internal require of
1603+ // sigstore is intercepted: a user-supplied bundle is only verified,
1604+ // generation (attest) must never run
1605+ libnpmpublish : t . mock ( 'libnpmpublish' , {
1606+ 'libnpmpublish/lib/provenance' : t . mock ( 'libnpmpublish/lib/provenance' , {
1607+ sigstore : {
1608+ verify : async ( ) => { } ,
1609+ attest : async ( ) => {
1610+ throw new Error ( 'sigstore.attest must not be called when provenance-file is configured' )
1611+ } ,
15821612 } ,
1583- } ,
1613+ } ) ,
15841614 } ) ,
1585- } ) ,
1615+ } ,
15861616 } ,
1587- } ,
1588- } )
1617+ } )
15891618
1590- // compute the tarball integrity the same way libnpmpublish does so the
1591- // provenance bundle subject matches the packed tarball
1592- packMock . tarballData = await pacote . tarball ( prefix , { Arborist } )
1593- const integrity = ssri . fromData ( packMock . tarballData , { algorithms : [ 'sha512' ] } )
1594- const spec = npa . resolve ( pkg , '1.0.0' )
1595- const provenanceBundle = {
1596- mediaType : 'application/vnd.dev.sigstore.bundle+json;version=0.2' ,
1597- verificationMaterial : {
1598- x509CertificateChain : {
1599- certificates : [ { rawBytes : 'dGVzdA==' } ] ,
1619+ // compute the tarball integrity the same way libnpmpublish does so the
1620+ // provenance bundle subject matches the packed tarball
1621+ packMock . tarballData = await pacote . tarball ( prefix , { Arborist } )
1622+ const integrity = ssri . fromData ( packMock . tarballData , { algorithms : [ 'sha512' ] } )
1623+ const spec = npa . resolve ( pkg , '1.0.0' )
1624+ const provenanceBundle = {
1625+ mediaType : 'application/vnd.dev.sigstore.bundle+json;version=0.2' ,
1626+ verificationMaterial : {
1627+ x509CertificateChain : {
1628+ certificates : [ { rawBytes : 'dGVzdA==' } ] ,
1629+ } ,
1630+ tlogEntries : [ ] ,
16001631 } ,
1601- tlogEntries : [ ] ,
1602- } ,
1603- dsseEnvelope : {
1604- payload : Buffer . from ( JSON . stringify ( {
1605- _type : 'https://in-toto.io/Statement/v0.1' ,
1606- subject : [
1607- {
1608- name : npa . toPurl ( spec ) ,
1609- digest : { sha512 : integrity . sha512 [ 0 ] . hexDigest ( ) } ,
1610- } ,
1611- ] ,
1612- predicateType : 'https://slsa.dev/provenance/v0.2' ,
1613- predicate : { } ,
1614- } ) ) . toString ( 'base64' ) ,
1615- payloadType : 'application/vnd.in-toto+json' ,
1616- signatures : [ {
1617- /* eslint-disable-next-line max-len */
1618- sig : 'MEUCIQDqHtpkk1d0rMGLmf3qet9jLale3KVn8Pnywpwt7ln+9AIgG9CJvvUmyemhNYHz0DfJ4vMfKk1TMg+m3hR0mISXJos=' ,
1619- keyid : '' ,
1620- } ] ,
1621- } ,
1622- }
1623- fs . writeFileSync ( provenanceBundlePath , JSON . stringify ( provenanceBundle , null , 2 ) )
1632+ dsseEnvelope : {
1633+ payload : Buffer . from ( JSON . stringify ( {
1634+ _type : 'https://in-toto.io/Statement/v0.1' ,
1635+ subject : [
1636+ {
1637+ name : npa . toPurl ( spec ) ,
1638+ digest : { sha512 : integrity . sha512 [ 0 ] . hexDigest ( ) } ,
1639+ } ,
1640+ ] ,
1641+ predicateType : 'https://slsa.dev/provenance/v0.2' ,
1642+ predicate : { } ,
1643+ } ) ) . toString ( 'base64' ) ,
1644+ payloadType : 'application/vnd.in-toto+json' ,
1645+ signatures : [ {
1646+ /* eslint-disable-next-line max-len */
1647+ sig : 'MEUCIQDqHtpkk1d0rMGLmf3qet9jLale3KVn8Pnywpwt7ln+9AIgG9CJvvUmyemhNYHz0DfJ4vMfKk1TMg+m3hR0mISXJos=' ,
1648+ keyid : '' ,
1649+ } ] ,
1650+ } ,
1651+ }
1652+ fs . writeFileSync ( provenanceBundlePath , JSON . stringify ( provenanceBundle , null , 2 ) )
1653+
1654+ let publishedBody
1655+ registry . nock
1656+ . put ( `/${ spec . escapedName } ` , ( body ) => {
1657+ publishedBody = body
1658+ return true
1659+ } )
1660+ . matchHeader ( 'authorization' , 'Bearer exchange-token' )
1661+ // optional so a failed publish does not leave a pending mock behind
1662+ . optionally ( )
1663+ . reply ( 200 , { } )
1664+
1665+ // libnpmpublish checks package visibility itself before generating
1666+ // provenance; optional so it is only consumed if generation is attempted
1667+ registry . nock
1668+ . get ( `/-/package/${ spec . escapedName } /visibility` )
1669+ . optionally ( )
1670+ . reply ( 200 , { public : true } )
16241671
1625- let publishedBody
1626- registry . nock
1627- . put ( `/${ spec . escapedName } ` , ( body ) => {
1628- publishedBody = body
1629- return true
1630- } )
1631- . matchHeader ( 'authorization' , 'Bearer exchange-token' )
1632- // optional so a failed publish does not leave a pending mock behind
1633- . optionally ( )
1634- . reply ( 200 , { } )
1635-
1636- // libnpmpublish checks package visibility itself before generating
1637- // provenance; optional so it is only consumed if generation is attempted
1638- registry . nock
1639- . get ( `/-/package/${ spec . escapedName } /visibility` )
1640- . optionally ( )
1641- . reply ( 200 , { public : true } )
1672+ await npm . exec ( 'publish' , [ ] )
16421673
1643- await npm . exec ( 'publish' , [ ] )
1674+ t . match ( joinedOutput ( ) , '+ @npmcli/test-package@1.0.0' )
16441675
1645- t . match ( joinedOutput ( ) , '+ @npmcli/test-package@1.0.0' )
1646- const sigstoreAttachment = publishedBody ?. _attachments [ `${ pkg } -1.0.0.sigstore` ]
1647- t . ok ( sigstoreAttachment , 'published packument includes a sigstore bundle attachment' )
1648- t . strictSame (
1649- JSON . parse ( sigstoreAttachment . data ) ,
1650- provenanceBundle ,
1651- 'published sigstore bundle is the user-supplied provenance file'
1652- )
1653- } )
1676+ const attachment =
1677+ publishedBody ?. _attachments [ `${ pkg } -1.0.0.sigstore` ]
1678+
1679+ t . ok ( attachment , 'published packument includes supplied provenance' )
1680+ t . strictSame (
1681+ JSON . parse ( attachment . data ) ,
1682+ provenanceBundle ,
1683+ 'published sigstore bundle is the user-supplied provenance file'
1684+ )
1685+ } )
1686+ }
16541687
16551688 const brokenJwts = [
16561689 'x.invalid-jwt.x' ,
0 commit comments