Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/lib/content/commands/npm-trust.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ The `[package]` argument specifies the package name. If omitted, npm will use th

Each trust relationship has its own set of configuration options and flags based on the OIDC claims provided by that provider. OIDC claims come from the CI/CD provider and include information such as repository name, workflow file, or environment. Since each provider's claims differ, the available flags and configuration keys are not universal—npm matches the claims supported by each provider's OIDC configuration. For specific details on which claims and flags are supported for a given provider, use `npm trust <provider> --help`.

### Permissions

When creating a trust relationship, you must specify at least one permission flag to indicate which operations the trusted publisher is allowed to perform:

* `--allow-publish`: Allows the trusted publisher to run `npm publish` for the package.
* `--allow-stage-publish`: Allows the trusted publisher to run `npm stage` for the package. The alias `--allow-staged-publish` is also accepted.

At least one of these flags is required when creating a trust configuration. You can specify both to grant both permissions.

### Provider Options

The required options depend on the CI/CD provider you're configuring. Detailed information about each option is available in the [managing trusted publisher configurations](https://docs.npmjs.com/trusted-publishers#managing-trusted-publisher-configurations) section of the npm documentation. If a provider is repository-based and the option is not provided, npm will use the `repository.url` field from your `package.json`, if available.

Currently, the registry only supports one configuration per package. If you attempt to create a new trust relationship when one already exists, it will result in an error. To replace an existing configuration:
Expand Down
5 changes: 4 additions & 1 deletion lib/commands/trust/circleci.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Definition = require('@npmcli/config/lib/definitions/definition.js')
const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js')
const TrustCommand = require('../../trust-cmd.js')
const { trustDefinitions } = require('../../trust-cmd.js')

// UUID validation regex
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
Expand All @@ -13,7 +14,7 @@ class TrustCircleCI extends TrustCommand {
static providerEntity = 'CircleCI pipeline'

static usage = [
'[package] --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>...] [-y|--yes]',
'[package] --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>...] [--allow-publish] [--allow-stage-publish] [-y|--yes]',
]

static definitions = [
Expand Down Expand Up @@ -46,6 +47,8 @@ class TrustCircleCI extends TrustCommand {
type: [null, String, Array],
description: 'CircleCI context UUID to match',
}),
trustDefinitions['allow-publish'],
trustDefinitions['allow-stage-publish'],
// globals are alphabetical
globalDefinitions['dry-run'],
globalDefinitions.json,
Expand Down
5 changes: 4 additions & 1 deletion lib/commands/trust/github.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Definition = require('@npmcli/config/lib/definitions/definition.js')
const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js')
const TrustCommand = require('../../trust-cmd.js')
const { trustDefinitions } = require('../../trust-cmd.js')
const path = require('node:path')

class TrustGitHub extends TrustCommand {
Expand All @@ -16,7 +17,7 @@ class TrustGitHub extends TrustCommand {
static entityKey = 'repository'

static usage = [
'[package] --file [--repo|--repository] [--env|--environment] [-y|--yes]',
'[package] --file [--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]',
]

static definitions = [
Expand All @@ -38,6 +39,8 @@ class TrustGitHub extends TrustCommand {
description: 'CI environment name',
alias: ['env'],
}),
trustDefinitions['allow-publish'],
trustDefinitions['allow-stage-publish'],
// globals are alphabetical
globalDefinitions['dry-run'],
globalDefinitions.json,
Expand Down
5 changes: 4 additions & 1 deletion lib/commands/trust/gitlab.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Definition = require('@npmcli/config/lib/definitions/definition.js')
const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js')
const TrustCommand = require('../../trust-cmd.js')
const { trustDefinitions } = require('../../trust-cmd.js')
const path = require('node:path')

class TrustGitLab extends TrustCommand {
Expand All @@ -16,7 +17,7 @@ class TrustGitLab extends TrustCommand {
static entityKey = 'project'

static usage = [
'[package] --file [--project|--repo|--repository] [--env|--environment] [-y|--yes]',
'[package] --file [--project|--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]',
]

static definitions = [
Expand All @@ -37,6 +38,8 @@ class TrustGitLab extends TrustCommand {
description: 'CI environment name',
alias: ['env'],
}),
trustDefinitions['allow-publish'],
trustDefinitions['allow-stage-publish'],
// globals are alphabetical
globalDefinitions['dry-run'],
globalDefinitions.json,
Expand Down
69 changes: 65 additions & 4 deletions lib/trust-cmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,29 @@ const { read: _read } = require('read')
const { input, output, log, META } = require('proc-log')
const gitinfo = require('hosted-git-info')
const pkgJson = require('@npmcli/package-json')
const Definition = require('@npmcli/config/lib/definitions/definition.js')

const NPM_FRONTEND = 'https://www.npmjs.com'

const PERMISSIONS = {
Comment thread
wraithgar marked this conversation as resolved.
CREATE_PACKAGE: 'createPackage',
CREATE_STAGED_PACKAGE: 'createStagedPackage',
}

const trustDefinitions = {
'allow-publish': new Definition('allow-publish', {
default: false,
type: Boolean,
description: 'Allow npm publish for this trusted publisher configuration',
}),
'allow-stage-publish': new Definition('allow-stage-publish', {
default: false,
type: Boolean,
description: 'Allow npm stage publish for this trusted publisher configuration',
alias: ['allow-staged-publish'],
}),
}

class TrustCommand extends BaseCommand {
// Helper to format template strings with color
// Blue text with reset color for interpolated values
Expand Down Expand Up @@ -45,8 +65,22 @@ class TrustCommand extends BaseCommand {
}))
}

static permissionLabels = {
[PERMISSIONS.CREATE_PACKAGE]: 'publish',
[PERMISSIONS.CREATE_STAGED_PACKAGE]: 'stage publish',
}

static formatPermissions (permissions) {
if (!Array.isArray(permissions) || permissions.length === 0) {
return null
}
return permissions
.map(p => TrustCommand.permissionLabels[p] || p)
.join(', ')
}

logOptions (options, pad = true) {
const { values, warnings, fromPackageJson, urls } = { warnings: [], ...options }
const { values, warnings, fromPackageJson, urls, permissions } = { warnings: [], ...options }
if (warnings && warnings.length > 0) {
for (const warningMsg of warnings) {
log.warn('trust', warningMsg)
Expand All @@ -55,8 +89,12 @@ class TrustCommand extends BaseCommand {

const json = this.config.get('json')
if (json) {
const jsonValues = { ...options.values }
if (permissions) {
jsonValues.permissions = permissions
}
// Disable redaction: trust config values (e.g. CircleCI UUIDs) are not secrets
output.standard(JSON.stringify(options.values, null, 2), { [META]: true, redact: false })
output.standard(JSON.stringify(jsonValues, null, 2), { [META]: true, redact: false })
return
}

Expand All @@ -82,6 +120,10 @@ class TrustCommand extends BaseCommand {
lines.push(parts.join(' '))
}
}
const formattedPermissions = TrustCommand.formatPermissions(permissions)
if (formattedPermissions) {
lines.push(`${chalk.reset('permissions')}: ${chalk.green(formattedPermissions)}`)
}
if (pad) {
output.standard()
}
Expand Down Expand Up @@ -165,19 +207,36 @@ class TrustCommand extends BaseCommand {
const { providerName, providerEntity, providerHostname } = this.constructor
const dryRun = this.config.get('dry-run')
const yes = this.config.get('yes') // deep-lore this allows for --no-yes

const allowPublish = flags['allow-publish']
const allowStagePublish = flags['allow-stage-publish']

if (!allowPublish && !allowStagePublish) {
throw new Error('At least one permission flag is required (--allow-publish, --allow-stage-publish)')
}

const permissions = []
if (allowPublish) {
permissions.push(PERMISSIONS.CREATE_PACKAGE)
}
if (allowStagePublish) {
permissions.push(PERMISSIONS.CREATE_STAGED_PACKAGE)
}

const options = await this.flagsToOptions({ positionalArgs, flags, providerHostname })
this.dialogue`Establishing trust between ${options.values.package} package and ${providerName}`
this.dialogue`Anyone with ${providerEntity} write access can publish to ${options.values.package}`
this.dialogue`Two-factor authentication is required for this operation`
if (!this.registryIsDefault) {
this.warn`Registry ${this.npm.config.get('registry')} may not support trusted publishing`
}
this.logOptions(options)
this.logOptions({ ...options, permissions })
if (dryRun) {
return
}
await this.confirmOperation(yes)
const trustConfig = this.constructor.optionsToBody(options.values)
trustConfig.permissions = permissions
const response = await this.createConfig(options.values.package, [trustConfig])
const body = await response.json()
this.dialogue`Trust configuration created successfully for ${options.values.package} with the following settings:`
Expand Down Expand Up @@ -273,12 +332,14 @@ class TrustCommand extends BaseCommand {
const items = Array.isArray(body) ? body : [body]
for (const config of items) {
const values = this.constructor.bodyToOptions(config)
const permissions = config.permissions
output.standard()
this.logOptions({ values }, false)
this.logOptions({ values, permissions }, false)
}
output.standard()
}
}

module.exports = TrustCommand
module.exports.NPM_FRONTEND = NPM_FRONTEND
module.exports.trustDefinitions = trustDefinitions
12 changes: 12 additions & 0 deletions tap-snapshots/test/lib/commands/completion.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,16 @@ Array [
--repo
--environment
--env
--allow-publish
--allow-stage-publish
--allow-staged-publish
--dry-run
--json
--registry
--yes
--no-allow-publish
--no-allow-stage-publish
--no-allow-staged-publish
--no-dry-run
--no-json
--no-yes
Expand All @@ -123,10 +129,16 @@ Array [
--project
--environment
--env
--allow-publish
--allow-stage-publish
--allow-staged-publish
--dry-run
--json
--registry
--yes
--no-allow-publish
--no-allow-stage-publish
--no-allow-staged-publish
--no-dry-run
--no-json
--no-yes
Expand Down
6 changes: 3 additions & 3 deletions tap-snapshots/test/lib/docs.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5803,9 +5803,9 @@ exports[`test/lib/docs.js TAP usage trust > must match snapshot 1`] = `
Create a trusted relationship between a package and a OIDC provider

Usage:
npm trust github [package] --file [--repo|--repository] [--env|--environment] [-y|--yes]
npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [-y|--yes]
npm trust circleci [package] --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>...] [-y|--yes]
npm trust github [package] --file [--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]
npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]
npm trust circleci [package] --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>...] [--allow-publish] [--allow-stage-publish] [-y|--yes]
npm trust list [package]
npm trust revoke [package] --id=<trust-id>

Expand Down
11 changes: 11 additions & 0 deletions test/lib/commands/trust/circleci.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ t.test('circleci with all options provided', async t => {
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--vcs-origin', 'github.com/owner/repo',
'--context-id', '123e4567-e89b-12d3-a456-426614174000',
'--allow-publish',
])
})

Expand Down Expand Up @@ -85,6 +86,7 @@ t.test('circleci without optional context-id', async t => {
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--vcs-origin', 'github.com/owner/repo',
'--allow-publish',
])
})

Expand Down Expand Up @@ -128,6 +130,7 @@ t.test('circleci with multiple context-ids', async t => {
'--vcs-origin', 'github.com/owner/repo',
'--context-id', '123e4567-e89b-12d3-a456-426614174000',
'--context-id', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'--allow-publish',
])
})

Expand All @@ -152,6 +155,7 @@ t.test('circleci missing required org-id', async t => {
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--vcs-origin', 'github.com/owner/repo',
'--allow-publish',
]),
{ message: /org-id is required/ }
)
Expand All @@ -178,6 +182,7 @@ t.test('circleci missing required project-id', async t => {
'--org-id', '550e8400-e29b-41d4-a716-446655440000',
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--vcs-origin', 'github.com/owner/repo',
'--allow-publish',
]),
{ message: /project-id is required/ }
)
Expand All @@ -204,6 +209,7 @@ t.test('circleci missing required pipeline-definition-id', async t => {
'--org-id', '550e8400-e29b-41d4-a716-446655440000',
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
'--vcs-origin', 'github.com/owner/repo',
'--allow-publish',
]),
{ message: /pipeline-definition-id is required/ }
)
Expand All @@ -230,6 +236,7 @@ t.test('circleci missing required vcs-origin', async t => {
'--org-id', '550e8400-e29b-41d4-a716-446655440000',
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--allow-publish',
]),
{ message: /vcs-origin is required/ }
)
Expand Down Expand Up @@ -257,6 +264,7 @@ t.test('circleci with invalid org-id uuid format', async t => {
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--vcs-origin', 'github.com/owner/repo',
'--allow-publish',
]),
{ message: /org-id must be a valid UUID/ }
)
Expand Down Expand Up @@ -284,6 +292,7 @@ t.test('circleci with invalid vcs-origin format', async t => {
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--vcs-origin', 'invalid-format',
'--allow-publish',
]),
{ message: /vcs-origin must be in format 'provider\/owner\/repo'/ }
)
Expand Down Expand Up @@ -311,6 +320,7 @@ t.test('circleci with vcs-origin containing scheme prefix', async t => {
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--vcs-origin', 'https://github.com/owner/repo',
'--allow-publish',
]),
{ message: /vcs-origin must not include a scheme/ }
)
Expand All @@ -336,6 +346,7 @@ t.test('circleci missing package name', async t => {
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
'--vcs-origin', 'github.com/owner/repo',
'--allow-publish',
]),
{ message: /Package name must be specified either as an argument or in package.json file/ }
)
Expand Down
8 changes: 4 additions & 4 deletions test/lib/commands/trust/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ t.test('github with all options provided', async t => {

registry.trustCreate({ packageName })

await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--environment', 'production'])
await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--environment', 'production', '--allow-publish'])
})

t.test('github with invalid repository format', async t => {
Expand All @@ -61,7 +61,7 @@ t.test('github with invalid repository format', async t => {
})

await t.rejects(
npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'invalid']),
npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'invalid', '--allow-publish']),
{ message: /must be specified in the format owner\/repository/ }
)
})
Expand Down Expand Up @@ -89,7 +89,7 @@ t.test('github with file as path', async t => {
})

await t.rejects(
npm.exec('trust', ['github', packageName, '--yes', '--file', '.github/workflows/ci.yml', '--repository', 'owner/repo']),
npm.exec('trust', ['github', packageName, '--yes', '--file', '.github/workflows/ci.yml', '--repository', 'owner/repo', '--allow-publish']),
{ message: /must be just a file not a path/ }
)
})
Expand Down Expand Up @@ -124,7 +124,7 @@ t.test('github without environment', async t => {

registry.trustCreate({ packageName })

await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo'])
await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--allow-publish'])
})

t.test('bodyToOptions with all fields', t => {
Expand Down
Loading
Loading