1const libnpmaccess = require('libnpmaccess') 2const npa = require('npm-package-arg') 3const pkgJson = require('@npmcli/package-json') 4const localeCompare = require('@isaacs/string-locale-compare')('en') 5 6const otplease = require('../utils/otplease.js') 7const getIdentity = require('../utils/get-identity.js') 8const BaseCommand = require('../base-command.js') 9 10const commands = [ 11 'get', 12 'grant', 13 'list', 14 'revoke', 15 'set', 16] 17 18const setCommands = [ 19 'status=public', 20 'status=private', 21 'mfa=none', 22 'mfa=publish', 23 'mfa=automation', 24 '2fa=none', 25 '2fa=publish', 26 '2fa=automation', 27] 28 29class Access extends BaseCommand { 30 static description = 'Set access level on published packages' 31 static name = 'access' 32 static params = [ 33 'json', 34 'otp', 35 'registry', 36 ] 37 38 static usage = [ 39 'list packages [<user>|<scope>|<scope:team> [<package>]', 40 'list collaborators [<package> [<user>]]', 41 'get status [<package>]', 42 'set status=public|private [<package>]', 43 'set mfa=none|publish|automation [<package>]', 44 'grant <read-only|read-write> <scope:team> [<package>]', 45 'revoke <scope:team> [<package>]', 46 ] 47 48 static async completion (opts) { 49 const argv = opts.conf.argv.remain 50 if (argv.length === 2) { 51 return commands 52 } 53 54 if (argv.length === 3) { 55 switch (argv[2]) { 56 case 'grant': 57 return ['read-only', 'read-write'] 58 case 'revoke': 59 return [] 60 case 'list': 61 case 'ls': 62 return ['packages', 'collaborators'] 63 case 'get': 64 return ['status'] 65 case 'set': 66 return setCommands 67 default: 68 throw new Error(argv[2] + ' not recognized') 69 } 70 } 71 } 72 73 async exec ([cmd, subcmd, ...args]) { 74 if (!cmd) { 75 throw this.usageError() 76 } 77 if (!commands.includes(cmd)) { 78 throw this.usageError(`${cmd} is not a valid access command`) 79 } 80 // All commands take at least one more parameter so we can do this check up front 81 if (!subcmd) { 82 throw this.usageError() 83 } 84 85 switch (cmd) { 86 case 'grant': 87 if (!['read-only', 'read-write'].includes(subcmd)) { 88 throw this.usageError('grant must be either `read-only` or `read-write`') 89 } 90 if (!args[0]) { 91 throw this.usageError('`<scope:team>` argument is required') 92 } 93 return this.#grant(subcmd, args[0], args[1]) 94 case 'revoke': 95 return this.#revoke(subcmd, args[0]) 96 case 'list': 97 case 'ls': 98 if (subcmd === 'packages') { 99 return this.#listPackages(args[0], args[1]) 100 } 101 if (subcmd === 'collaborators') { 102 return this.#listCollaborators(args[0], args[1]) 103 } 104 throw this.usageError(`list ${subcmd} is not a valid access command`) 105 case 'get': 106 if (subcmd !== 'status') { 107 throw this.usageError(`get ${subcmd} is not a valid access command`) 108 } 109 return this.#getStatus(args[0]) 110 case 'set': 111 if (!setCommands.includes(subcmd)) { 112 throw this.usageError(`set ${subcmd} is not a valid access command`) 113 } 114 return this.#set(subcmd, args[0]) 115 } 116 } 117 118 async #grant (permissions, scope, pkg) { 119 await libnpmaccess.setPermissions(scope, pkg, permissions, this.npm.flatOptions) 120 } 121 122 async #revoke (scope, pkg) { 123 await libnpmaccess.removePermissions(scope, pkg, this.npm.flatOptions) 124 } 125 126 async #listPackages (owner, pkg) { 127 if (!owner) { 128 owner = await getIdentity(this.npm, this.npm.flatOptions) 129 } 130 const pkgs = await libnpmaccess.getPackages(owner, this.npm.flatOptions) 131 this.#output(pkgs, pkg) 132 } 133 134 async #listCollaborators (pkg, user) { 135 const pkgName = await this.#getPackage(pkg, false) 136 const collabs = await libnpmaccess.getCollaborators(pkgName, this.npm.flatOptions) 137 this.#output(collabs, user) 138 } 139 140 async #getStatus (pkg) { 141 const pkgName = await this.#getPackage(pkg, false) 142 const visibility = await libnpmaccess.getVisibility(pkgName, this.npm.flatOptions) 143 this.#output({ [pkgName]: visibility.public ? 'public' : 'private' }) 144 } 145 146 async #set (subcmd, pkg) { 147 const [subkey, subval] = subcmd.split('=') 148 switch (subkey) { 149 case 'mfa': 150 case '2fa': 151 return this.#setMfa(pkg, subval) 152 case 'status': 153 return this.#setStatus(pkg, subval) 154 } 155 } 156 157 async #setMfa (pkg, level) { 158 const pkgName = await this.#getPackage(pkg, false) 159 await otplease(this.npm, this.npm.flatOptions, (opts) => { 160 return libnpmaccess.setMfa(pkgName, level, opts) 161 }) 162 } 163 164 async #setStatus (pkg, status) { 165 // only scoped packages can have their access changed 166 const pkgName = await this.#getPackage(pkg, true) 167 if (status === 'private') { 168 status = 'restricted' 169 } 170 await otplease(this.npm, this.npm.flatOptions, (opts) => { 171 return libnpmaccess.setAccess(pkgName, status, opts) 172 }) 173 return this.#getStatus(pkgName) 174 } 175 176 async #getPackage (name, requireScope) { 177 if (!name) { 178 try { 179 const { content } = await pkgJson.normalize(this.npm.prefix) 180 name = content.name 181 } catch (err) { 182 if (err.code === 'ENOENT') { 183 throw Object.assign(new Error('no package name given and no package.json found'), { 184 code: 'ENOENT', 185 }) 186 } else { 187 throw err 188 } 189 } 190 } 191 192 const spec = npa(name) 193 if (requireScope && !spec.scope) { 194 throw this.usageError('This command is only available for scoped packages.') 195 } 196 return name 197 } 198 199 #output (items, limiter) { 200 const output = {} 201 const lookup = { 202 __proto__: null, 203 read: 'read-only', 204 write: 'read-write', 205 } 206 for (const item in items) { 207 const val = items[item] 208 output[item] = lookup[val] || val 209 } 210 if (this.npm.config.get('json')) { 211 this.npm.output(JSON.stringify(output, null, 2)) 212 } else { 213 for (const item of Object.keys(output).sort(localeCompare)) { 214 if (!limiter || limiter === item) { 215 this.npm.output(`${item}: ${output[item]}`) 216 } 217 } 218 } 219 } 220} 221 222module.exports = Access 223