feat: add permissions support to trust commands (#9248) · npm/cli@cf94dbe · GitHub
Skip to content

Commit cf94dbe

Browse files
reggiCopilot
andauthored
feat: add permissions support to trust commands (#9248)
## Summary Adds permission flags to trust create operations. Users must now specify at least one of `--allow-publish` or `--allow-stage-publish` (alias: `--allow-staged-publish`) when creating trust configurations. ## Changes - Add `--allow-publish` and `--allow-stage-publish` flags to all trust provider commands (GitHub, GitLab, CircleCI) - Require at least one permission flag when creating trust configurations - Include permissions in the request body and display output - Add `PERMISSIONS` constants for permission values - Update tests and completion snapshots for new flags ## Related - #9201 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c97b39b commit cf94dbe

11 files changed

Lines changed: 241 additions & 42 deletions

File tree

docs/lib/content/commands/npm-trust.md

Lines changed: 11 additions & 0 deletions

lib/commands/trust/circleci.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const Definition = require('@npmcli/config/lib/definitions/definition.js')
22
const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js')
33
const TrustCommand = require('../../trust-cmd.js')
4+
const { trustDefinitions } = require('../../trust-cmd.js')
45

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

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

1920
static definitions = [
@@ -46,6 +47,8 @@ class TrustCircleCI extends TrustCommand {
4647
type: [null, String, Array],
4748
description: 'CircleCI context UUID to match',
4849
}),
50+
trustDefinitions['allow-publish'],
51+
trustDefinitions['allow-stage-publish'],
4952
// globals are alphabetical
5053
globalDefinitions['dry-run'],
5154
globalDefinitions.json,

lib/commands/trust/github.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const Definition = require('@npmcli/config/lib/definitions/definition.js')
22
const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js')
33
const TrustCommand = require('../../trust-cmd.js')
4+
const { trustDefinitions } = require('../../trust-cmd.js')
45
const path = require('node:path')
56

67
class TrustGitHub extends TrustCommand {
@@ -16,7 +17,7 @@ class TrustGitHub extends TrustCommand {
1617
static entityKey = 'repository'
1718

1819
static usage = [
19-
'[package] --file [--repo|--repository] [--env|--environment] [-y|--yes]',
20+
'[package] --file [--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]',
2021
]
2122

2223
static definitions = [
@@ -38,6 +39,8 @@ class TrustGitHub extends TrustCommand {
3839
description: 'CI environment name',
3940
alias: ['env'],
4041
}),
42+
trustDefinitions['allow-publish'],
43+
trustDefinitions['allow-stage-publish'],
4144
// globals are alphabetical
4245
globalDefinitions['dry-run'],
4346
globalDefinitions.json,

lib/commands/trust/gitlab.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const Definition = require('@npmcli/config/lib/definitions/definition.js')
22
const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js')
33
const TrustCommand = require('../../trust-cmd.js')
4+
const { trustDefinitions } = require('../../trust-cmd.js')
45
const path = require('node:path')
56

67
class TrustGitLab extends TrustCommand {
@@ -16,7 +17,7 @@ class TrustGitLab extends TrustCommand {
1617
static entityKey = 'project'
1718

1819
static usage = [
19-
'[package] --file [--project|--repo|--repository] [--env|--environment] [-y|--yes]',
20+
'[package] --file [--project|--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]',
2021
]
2122

2223
static definitions = [
@@ -37,6 +38,8 @@ class TrustGitLab extends TrustCommand {
3738
description: 'CI environment name',
3839
alias: ['env'],
3940
}),
41+
trustDefinitions['allow-publish'],
42+
trustDefinitions['allow-stage-publish'],
4043
// globals are alphabetical
4144
globalDefinitions['dry-run'],
4245
globalDefinitions.json,

lib/trust-cmd.js

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,29 @@ const { read: _read } = require('read')
66
const { input, output, log, META } = require('proc-log')
77
const gitinfo = require('hosted-git-info')
88
const pkgJson = require('@npmcli/package-json')
9+
const Definition = require('@npmcli/config/lib/definitions/definition.js')
910

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

13+
const PERMISSIONS = {
14+
CREATE_PACKAGE: 'createPackage',
15+
CREATE_STAGED_PACKAGE: 'createStagedPackage',
16+
}
17+
18+
const trustDefinitions = {
19+
'allow-publish': new Definition('allow-publish', {
20+
default: false,
21+
type: Boolean,
22+
description: 'Allow npm publish for this trusted publisher configuration',
23+
}),
24+
'allow-stage-publish': new Definition('allow-stage-publish', {
25+
default: false,
26+
type: Boolean,
27+
description: 'Allow npm stage publish for this trusted publisher configuration',
28+
alias: ['allow-staged-publish'],
29+
}),
30+
}
31+
1232
class TrustCommand extends BaseCommand {
1333
// Helper to format template strings with color
1434
// Blue text with reset color for interpolated values
@@ -45,8 +65,22 @@ class TrustCommand extends BaseCommand {
4565
}))
4666
}
4767

68+
static permissionLabels = {
69+
[PERMISSIONS.CREATE_PACKAGE]: 'publish',
70+
[PERMISSIONS.CREATE_STAGED_PACKAGE]: 'stage publish',
71+
}
72+
73+
static formatPermissions (permissions) {
74+
if (!Array.isArray(permissions) || permissions.length === 0) {
75+
return null
76+
}
77+
return permissions
78+
.map(p => TrustCommand.permissionLabels[p] || p)
79+
.join(', ')
80+
}
81+
4882
logOptions (options, pad = true) {
49-
const { values, warnings, fromPackageJson, urls } = { warnings: [], ...options }
83+
const { values, warnings, fromPackageJson, urls, permissions } = { warnings: [], ...options }
5084
if (warnings && warnings.length > 0) {
5185
for (const warningMsg of warnings) {
5286
log.warn('trust', warningMsg)
@@ -55,8 +89,12 @@ class TrustCommand extends BaseCommand {
5589

5690
const json = this.config.get('json')
5791
if (json) {
92+
const jsonValues = { ...options.values }
93+
if (permissions) {
94+
jsonValues.permissions = permissions
95+
}
5896
// Disable redaction: trust config values (e.g. CircleCI UUIDs) are not secrets
59-
output.standard(JSON.stringify(options.values, null, 2), { [META]: true, redact: false })
97+
output.standard(JSON.stringify(jsonValues, null, 2), { [META]: true, redact: false })
6098
return
6199
}
62100

@@ -82,6 +120,10 @@ class TrustCommand extends BaseCommand {
82120
lines.push(parts.join(' '))
83121
}
84122
}
123+
const formattedPermissions = TrustCommand.formatPermissions(permissions)
124+
if (formattedPermissions) {
125+
lines.push(`${chalk.reset('permissions')}: ${chalk.green(formattedPermissions)}`)
126+
}
85127
if (pad) {
86128
output.standard()
87129
}
@@ -165,19 +207,36 @@ class TrustCommand extends BaseCommand {
165207
const { providerName, providerEntity, providerHostname } = this.constructor
166208
const dryRun = this.config.get('dry-run')
167209
const yes = this.config.get('yes') // deep-lore this allows for --no-yes
210+
211+
const allowPublish = flags['allow-publish']
212+
const allowStagePublish = flags['allow-stage-publish']
213+
214+
if (!allowPublish && !allowStagePublish) {
215+
throw new Error('At least one permission flag is required (--allow-publish, --allow-stage-publish)')
216+
}
217+
218+
const permissions = []
219+
if (allowPublish) {
220+
permissions.push(PERMISSIONS.CREATE_PACKAGE)
221+
}
222+
if (allowStagePublish) {
223+
permissions.push(PERMISSIONS.CREATE_STAGED_PACKAGE)
224+
}
225+
168226
const options = await this.flagsToOptions({ positionalArgs, flags, providerHostname })
169227
this.dialogue`Establishing trust between ${options.values.package} package and ${providerName}`
170228
this.dialogue`Anyone with ${providerEntity} write access can publish to ${options.values.package}`
171229
this.dialogue`Two-factor authentication is required for this operation`
172230
if (!this.registryIsDefault) {
173231
this.warn`Registry ${this.npm.config.get('registry')} may not support trusted publishing`
174232
}
175-
this.logOptions(options)
233+
this.logOptions({ ...options, permissions })
176234
if (dryRun) {
177235
return
178236
}
179237
await this.confirmOperation(yes)
180238
const trustConfig = this.constructor.optionsToBody(options.values)
239+
trustConfig.permissions = permissions
181240
const response = await this.createConfig(options.values.package, [trustConfig])
182241
const body = await response.json()
183242
this.dialogue`Trust configuration created successfully for ${options.values.package} with the following settings:`
@@ -273,12 +332,14 @@ class TrustCommand extends BaseCommand {
273332
const items = Array.isArray(body) ? body : [body]
274333
for (const config of items) {
275334
const values = this.constructor.bodyToOptions(config)
335+
const permissions = config.permissions
276336
output.standard()
277-
this.logOptions({ values }, false)
337+
this.logOptions({ values, permissions }, false)
278338
}
279339
output.standard()
280340
}
281341
}
282342

283343
module.exports = TrustCommand
284344
module.exports.NPM_FRONTEND = NPM_FRONTEND
345+
module.exports.trustDefinitions = trustDefinitions

tap-snapshots/test/lib/commands/completion.js.test.cjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,16 @@ Array [
103103
--repo
104104
--environment
105105
--env
106+
--allow-publish
107+
--allow-stage-publish
108+
--allow-staged-publish
106109
--dry-run
107110
--json
108111
--registry
109112
--yes
113+
--no-allow-publish
114+
--no-allow-stage-publish
115+
--no-allow-staged-publish
110116
--no-dry-run
111117
--no-json
112118
--no-yes
@@ -121,10 +127,16 @@ Array [
121127
--project
122128
--environment
123129
--env
130+
--allow-publish
131+
--allow-stage-publish
132+
--allow-staged-publish
124133
--dry-run
125134
--json
126135
--registry
127136
--yes
137+
--no-allow-publish
138+
--no-allow-stage-publish
139+
--no-allow-staged-publish
128140
--no-dry-run
129141
--no-json
130142
--no-yes

tap-snapshots/test/lib/docs.js.test.cjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5821,9 +5821,9 @@ exports[`test/lib/docs.js TAP usage trust > must match snapshot 1`] = `
58215821
Create a trusted relationship between a package and a OIDC provider
58225822
58235823
Usage:
5824-
npm trust github [package] --file [--repo|--repository] [--env|--environment] [-y|--yes]
5825-
npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [-y|--yes]
5826-
npm trust circleci [package] --org-id <uuid> --project-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>...] [-y|--yes]
5824+
npm trust github [package] --file [--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]
5825+
npm trust gitlab [package] --file [--project|--repo|--repository] [--env|--environment] [--allow-publish] [--allow-stage-publish] [-y|--yes]
5826+
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]
58275827
npm trust list [package]
58285828
npm trust revoke [package] --id=<trust-id>
58295829

test/lib/commands/trust/circleci.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ t.test('circleci with all options provided', async t => {
4444
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
4545
'--vcs-origin', 'github.com/owner/repo',
4646
'--context-id', '123e4567-e89b-12d3-a456-426614174000',
47+
'--allow-publish',
4748
])
4849
})
4950

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

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

@@ -152,6 +155,7 @@ t.test('circleci missing required org-id', async t => {
152155
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
153156
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
154157
'--vcs-origin', 'github.com/owner/repo',
158+
'--allow-publish',
155159
]),
156160
{ message: /org-id is required/ }
157161
)
@@ -178,6 +182,7 @@ t.test('circleci missing required project-id', async t => {
178182
'--org-id', '550e8400-e29b-41d4-a716-446655440000',
179183
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
180184
'--vcs-origin', 'github.com/owner/repo',
185+
'--allow-publish',
181186
]),
182187
{ message: /project-id is required/ }
183188
)
@@ -204,6 +209,7 @@ t.test('circleci missing required pipeline-definition-id', async t => {
204209
'--org-id', '550e8400-e29b-41d4-a716-446655440000',
205210
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
206211
'--vcs-origin', 'github.com/owner/repo',
212+
'--allow-publish',
207213
]),
208214
{ message: /pipeline-definition-id is required/ }
209215
)
@@ -230,6 +236,7 @@ t.test('circleci missing required vcs-origin', async t => {
230236
'--org-id', '550e8400-e29b-41d4-a716-446655440000',
231237
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
232238
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
239+
'--allow-publish',
233240
]),
234241
{ message: /vcs-origin is required/ }
235242
)
@@ -257,6 +264,7 @@ t.test('circleci with invalid org-id uuid format', async t => {
257264
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
258265
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
259266
'--vcs-origin', 'github.com/owner/repo',
267+
'--allow-publish',
260268
]),
261269
{ message: /org-id must be a valid UUID/ }
262270
)
@@ -284,6 +292,7 @@ t.test('circleci with invalid vcs-origin format', async t => {
284292
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
285293
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
286294
'--vcs-origin', 'invalid-format',
295+
'--allow-publish',
287296
]),
288297
{ message: /vcs-origin must be in format 'provider\/owner\/repo'/ }
289298
)
@@ -311,6 +320,7 @@ t.test('circleci with vcs-origin containing scheme prefix', async t => {
311320
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
312321
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
313322
'--vcs-origin', 'https://github.com/owner/repo',
323+
'--allow-publish',
314324
]),
315325
{ message: /vcs-origin must not include a scheme/ }
316326
)
@@ -336,6 +346,7 @@ t.test('circleci missing package name', async t => {
336346
'--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7',
337347
'--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
338348
'--vcs-origin', 'github.com/owner/repo',
349+
'--allow-publish',
339350
]),
340351
{ message: /Package name must be specified either as an argument or in package.json file/ }
341352
)

test/lib/commands/trust/github.js

Lines changed: 4 additions & 4 deletions

0 commit comments

Comments
 (0)