1const npa = require('npm-package-arg') 2const npmFetch = require('npm-registry-fetch') 3const pacote = require('pacote') 4const log = require('../utils/log-shim') 5const otplease = require('../utils/otplease.js') 6const pkgJson = require('@npmcli/package-json') 7const BaseCommand = require('../base-command.js') 8 9const readJson = async (path) => { 10 try { 11 const { content } = await pkgJson.normalize(path) 12 return content 13 } catch { 14 return {} 15 } 16} 17 18class Owner extends BaseCommand { 19 static description = 'Manage package owners' 20 static name = 'owner' 21 static params = [ 22 'registry', 23 'otp', 24 'workspace', 25 'workspaces', 26 ] 27 28 static usage = [ 29 'add <user> <package-spec>', 30 'rm <user> <package-spec>', 31 'ls <package-spec>', 32 ] 33 34 static workspaces = true 35 static ignoreImplicitWorkspace = false 36 37 static async completion (opts, npm) { 38 const argv = opts.conf.argv.remain 39 if (argv.length > 3) { 40 return [] 41 } 42 43 if (argv[1] !== 'owner') { 44 argv.unshift('owner') 45 } 46 47 if (argv.length === 2) { 48 return ['add', 'rm', 'ls'] 49 } 50 51 // reaches registry in order to autocomplete rm 52 if (argv[2] === 'rm') { 53 if (npm.global) { 54 return [] 55 } 56 const { name } = await readJson(npm.prefix) 57 if (!name) { 58 return [] 59 } 60 61 const spec = npa(name) 62 const data = await pacote.packument(spec, { 63 ...npm.flatOptions, 64 fullMetadata: true, 65 }) 66 if (data && data.maintainers && data.maintainers.length) { 67 return data.maintainers.map(m => m.name) 68 } 69 } 70 return [] 71 } 72 73 async exec ([action, ...args]) { 74 if (action === 'ls' || action === 'list') { 75 await this.ls(args[0]) 76 } else if (action === 'add') { 77 await this.changeOwners(args[0], args[1], 'add') 78 } else if (action === 'rm' || action === 'remove') { 79 await this.changeOwners(args[0], args[1], 'rm') 80 } else { 81 throw this.usageError() 82 } 83 } 84 85 async execWorkspaces ([action, ...args]) { 86 await this.setWorkspaces() 87 // ls pkg or owner add/rm package 88 if ((action === 'ls' && args.length > 0) || args.length > 1) { 89 const implicitWorkspaces = this.npm.config.get('workspace', 'default') 90 if (implicitWorkspaces.length === 0) { 91 log.warn(`Ignoring specified workspace(s)`) 92 } 93 return this.exec([action, ...args]) 94 } 95 96 for (const [name] of this.workspaces) { 97 if (action === 'ls' || action === 'list') { 98 await this.ls(name) 99 } else if (action === 'add') { 100 await this.changeOwners(args[0], name, 'add') 101 } else if (action === 'rm' || action === 'remove') { 102 await this.changeOwners(args[0], name, 'rm') 103 } else { 104 throw this.usageError() 105 } 106 } 107 } 108 109 async ls (pkg) { 110 pkg = await this.getPkg(this.npm.prefix, pkg) 111 const spec = npa(pkg) 112 113 try { 114 const packumentOpts = { ...this.npm.flatOptions, fullMetadata: true, preferOnline: true } 115 const { maintainers } = await pacote.packument(spec, packumentOpts) 116 if (!maintainers || !maintainers.length) { 117 this.npm.output('no admin found') 118 } else { 119 this.npm.output(maintainers.map(m => `${m.name} <${m.email}>`).join('\n')) 120 } 121 } catch (err) { 122 log.error('owner ls', "Couldn't get owner data", npmFetch.cleanUrl(pkg)) 123 throw err 124 } 125 } 126 127 async getPkg (prefix, pkg) { 128 if (!pkg) { 129 if (this.npm.global) { 130 throw this.usageError() 131 } 132 const { name } = await readJson(prefix) 133 if (!name) { 134 throw this.usageError() 135 } 136 137 return name 138 } 139 return pkg 140 } 141 142 async changeOwners (user, pkg, addOrRm) { 143 if (!user) { 144 throw this.usageError() 145 } 146 147 pkg = await this.getPkg(this.npm.prefix, pkg) 148 log.verbose(`owner ${addOrRm}`, '%s to %s', user, pkg) 149 150 const spec = npa(pkg) 151 const uri = `/-/user/org.couchdb.user:${encodeURIComponent(user)}` 152 let u 153 154 try { 155 u = await npmFetch.json(uri, this.npm.flatOptions) 156 } catch (err) { 157 log.error('owner mutate', `Error getting user data for ${user}`) 158 throw err 159 } 160 161 // normalize user data 162 u = { name: u.name, email: u.email } 163 164 const data = await pacote.packument(spec, { 165 ...this.npm.flatOptions, 166 fullMetadata: true, 167 preferOnline: true, 168 }) 169 170 const owners = data.maintainers || [] 171 let maintainers 172 if (addOrRm === 'add') { 173 const existing = owners.find(o => o.name === u.name) 174 if (existing) { 175 log.info( 176 'owner add', 177 `Already a package owner: ${existing.name} <${existing.email}>` 178 ) 179 return 180 } 181 maintainers = [ 182 ...owners, 183 u, 184 ] 185 } else { 186 maintainers = owners.filter(o => o.name !== u.name) 187 188 if (maintainers.length === owners.length) { 189 log.info('owner rm', 'Not a package owner: ' + u.name) 190 return false 191 } 192 193 if (!maintainers.length) { 194 throw Object.assign( 195 new Error( 196 'Cannot remove all owners of a package. Add someone else first.' 197 ), 198 { code: 'EOWNERRM' } 199 ) 200 } 201 } 202 203 const dataPath = `/${spec.escapedName}/-rev/${encodeURIComponent(data._rev)}` 204 try { 205 const res = await otplease(this.npm, this.npm.flatOptions, opts => { 206 return npmFetch.json(dataPath, { 207 ...opts, 208 method: 'PUT', 209 body: { 210 _id: data._id, 211 _rev: data._rev, 212 maintainers, 213 }, 214 spec, 215 }) 216 }) 217 if (addOrRm === 'add') { 218 this.npm.output(`+ ${user} (${spec.name})`) 219 } else { 220 this.npm.output(`- ${user} (${spec.name})`) 221 } 222 return res 223 } catch (err) { 224 throw Object.assign( 225 new Error('Failed to update package: ' + JSON.stringify(err.message)), 226 { code: 'EOWNERMUTATE' } 227 ) 228 } 229 } 230} 231 232module.exports = Owner 233