owner.js 6.2 KB

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