|
1 | 1 | 'use strict'
|
2 | 2 | /* eslint-disable standard/no-callback-literal */
|
3 | 3 |
|
4 |
| -var resolve = require('path').resolve |
| 4 | +const BB = require('bluebird') |
5 | 5 |
|
6 |
| -var readPackageJson = require('read-package-json') |
7 |
| -var mapToRegistry = require('./utils/map-to-registry.js') |
8 |
| -var npm = require('./npm.js') |
9 |
| -var output = require('./utils/output.js') |
10 |
| - |
11 |
| -var whoami = require('./whoami') |
| 6 | +const figgyPudding = require('figgy-pudding') |
| 7 | +const libaccess = require('libnpmaccess') |
| 8 | +const npmConfig = require('./config/figgy-config.js') |
| 9 | +const output = require('./utils/output.js') |
| 10 | +const otplease = require('./utils/otplease.js') |
| 11 | +const path = require('path') |
| 12 | +const prefix = require('./npm.js').prefix |
| 13 | +const readPackageJson = BB.promisify(require('read-package-json')) |
| 14 | +const usage = require('./utils/usage.js') |
| 15 | +const whoami = require('./whoami.js') |
12 | 16 |
|
13 | 17 | module.exports = access
|
14 | 18 |
|
15 |
| -access.usage = |
| 19 | +access.usage = usage( |
| 20 | + 'npm access', |
16 | 21 | 'npm access public [<package>]\n' +
|
17 | 22 | 'npm access restricted [<package>]\n' +
|
18 | 23 | 'npm access grant <read-only|read-write> <scope:team> [<package>]\n' +
|
19 | 24 | 'npm access revoke <scope:team> [<package>]\n' +
|
| 25 | + 'npm access 2fa-required [<package>]\n' + |
| 26 | + 'npm access 2fa-not-required [<package>]\n' + |
20 | 27 | 'npm access ls-packages [<user>|<scope>|<scope:team>]\n' +
|
21 | 28 | 'npm access ls-collaborators [<package> [<user>]]\n' +
|
22 | 29 | 'npm access edit [<package>]'
|
| 30 | +) |
| 31 | + |
| 32 | +access.subcommands = [ |
| 33 | + 'public', 'restricted', 'grant', 'revoke', |
| 34 | + 'ls-packages', 'ls-collaborators', 'edit' |
| 35 | +] |
| 36 | + |
| 37 | +const AccessConfig = figgyPudding({ |
| 38 | + json: {} |
| 39 | +}) |
23 | 40 |
|
24 |
| -access.subcommands = ['public', 'restricted', 'grant', 'revoke', |
25 |
| - 'ls-packages', 'ls-collaborators', 'edit'] |
| 41 | +function UsageError (msg = '') { |
| 42 | + throw Object.assign(new Error( |
| 43 | + (msg ? `\nUsage: ${msg}\n\n` : '') + |
| 44 | + access.usage |
| 45 | + ), {code: 'EUSAGE'}) |
| 46 | +} |
26 | 47 |
|
27 | 48 | access.completion = function (opts, cb) {
|
28 | 49 | var argv = opts.conf.argv.remain
|
@@ -50,81 +71,124 @@ access.completion = function (opts, cb) {
|
50 | 71 | }
|
51 | 72 | }
|
52 | 73 |
|
53 |
| -function access (args, cb) { |
54 |
| - var cmd = args.shift() |
55 |
| - var params |
56 |
| - return parseParams(cmd, args, function (err, p) { |
57 |
| - if (err) { return cb(err) } |
58 |
| - params = p |
59 |
| - return mapToRegistry(params.package, npm.config, invokeCmd) |
60 |
| - }) |
| 74 | +function access ([cmd, ...args], cb) { |
| 75 | + return BB.try(() => { |
| 76 | + const fn = access.subcommands.includes(cmd) && access[cmd] |
| 77 | + if (!cmd) { UsageError('Subcommand is required.') } |
| 78 | + if (!fn) { UsageError(`${cmd} is not a recognized subcommand.`) } |
61 | 79 |
|
62 |
| - function invokeCmd (err, uri, auth, base) { |
63 |
| - if (err) { return cb(err) } |
64 |
| - params.auth = auth |
65 |
| - try { |
66 |
| - return npm.registry.access(cmd, uri, params, function (err, data) { |
67 |
| - if (!err && data) { |
68 |
| - output(JSON.stringify(data, undefined, 2)) |
69 |
| - } |
70 |
| - cb(err, data) |
71 |
| - }) |
72 |
| - } catch (e) { |
73 |
| - cb(e.message + '\n\nUsage:\n' + access.usage) |
74 |
| - } |
75 |
| - } |
| 80 | + return fn(args, AccessConfig(npmConfig())) |
| 81 | + }).then( |
| 82 | + x => cb(null, x), |
| 83 | + err => err.code === 'EUSAGE' ? cb(err.message) : cb(err) |
| 84 | + ) |
76 | 85 | }
|
77 | 86 |
|
78 |
| -function parseParams (cmd, args, cb) { |
79 |
| - // mapToRegistry will complain if package is undefined, |
80 |
| - // but it's not needed for ls-packages |
81 |
| - var params = { 'package': '' } |
82 |
| - if (cmd === 'grant') { |
83 |
| - params.permissions = args.shift() |
84 |
| - } |
85 |
| - if (['grant', 'revoke', 'ls-packages'].indexOf(cmd) !== -1) { |
86 |
| - var entity = (args.shift() || '').split(':') |
87 |
| - params.scope = entity[0] |
88 |
| - params.team = entity[1] |
89 |
| - } |
| 87 | +access.public = ([pkg], opts) => { |
| 88 | + return modifyPackage(pkg, opts, libaccess.public) |
| 89 | +} |
90 | 90 |
|
91 |
| - if (cmd === 'ls-packages') { |
92 |
| - if (!params.scope) { |
93 |
| - whoami([], true, function (err, scope) { |
94 |
| - params.scope = scope |
95 |
| - cb(err, params) |
96 |
| - }) |
97 |
| - } else { |
98 |
| - cb(null, params) |
| 91 | +access.restricted = ([pkg], opts) => { |
| 92 | + return modifyPackage(pkg, opts, libaccess.restricted) |
| 93 | +} |
| 94 | + |
| 95 | +access.grant = ([perms, scopeteam, pkg], opts) => { |
| 96 | + return BB.try(() => { |
| 97 | + if (!perms || (perms !== 'read-only' && perms !== 'read-write')) { |
| 98 | + UsageError('First argument must be either `read-only` or `read-write.`') |
| 99 | + } |
| 100 | + if (!scopeteam) { |
| 101 | + UsageError('`<scope:team>` argument is required.') |
| 102 | + } |
| 103 | + const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] |
| 104 | + if (!scope && !team) { |
| 105 | + UsageError( |
| 106 | + 'Second argument used incorrect format.\n' + |
| 107 | + 'Example: @example:developers' |
| 108 | + ) |
99 | 109 | }
|
100 |
| - } else { |
101 |
| - getPackage(args.shift(), function (err, pkg) { |
102 |
| - if (err) return cb(err) |
103 |
| - params.package = pkg |
| 110 | + return modifyPackage(pkg, opts, (pkgName, opts) => { |
| 111 | + return libaccess.grant(pkgName, scope, team, perms, opts) |
| 112 | + }) |
| 113 | + }) |
| 114 | +} |
104 | 115 |
|
105 |
| - if (cmd === 'ls-collaborators') params.user = args.shift() |
106 |
| - cb(null, params) |
| 116 | +access.revoke = ([scopeteam, pkg], opts) => { |
| 117 | + return BB.try(() => { |
| 118 | + if (!scopeteam) { |
| 119 | + UsageError('`<scope:team>` argument is required.') |
| 120 | + } |
| 121 | + const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] |
| 122 | + if (!scope || !team) { |
| 123 | + UsageError( |
| 124 | + 'First argument used incorrect format.\n' + |
| 125 | + 'Example: @example:developers' |
| 126 | + ) |
| 127 | + } |
| 128 | + return modifyPackage(pkg, opts, (pkgName, opts) => { |
| 129 | + return libaccess.revoke(pkgName, scope, team, opts) |
107 | 130 | })
|
108 |
| - } |
| 131 | + }) |
| 132 | +} |
| 133 | + |
| 134 | +access['2fa-required'] = access.tfaRequired = ([pkg], opts) => { |
| 135 | + return modifyPackage(pkg, opts, libaccess.tfaRequired, false) |
| 136 | +} |
| 137 | + |
| 138 | +access['2fa-not-required'] = access.tfaNotRequired = ([pkg], opts) => { |
| 139 | + return modifyPackage(pkg, opts, libaccess.tfaNotRequired, false) |
109 | 140 | }
|
110 | 141 |
|
111 |
| -function getPackage (name, cb) { |
112 |
| - if (name && name.trim()) { |
113 |
| - cb(null, name.trim()) |
114 |
| - } else { |
115 |
| - readPackageJson( |
116 |
| - resolve(npm.prefix, 'package.json'), |
117 |
| - function (err, data) { |
118 |
| - if (err) { |
| 142 | +access['ls-packages'] = access.lsPackages = ([owner], opts) => { |
| 143 | + return ( |
| 144 | + owner ? BB.resolve(owner) : whoami([], true) |
| 145 | + ).then(owner => { |
| 146 | + const [, team] = owner.match(/^@?[^:]+:(.*)$/) || [] |
| 147 | + return libaccess.lsPackages(owner, team, opts) |
| 148 | + }).then(pkgs => { |
| 149 | + // TODO - print these out nicely (breaking change) |
| 150 | + output(JSON.stringify(pkgs, null, 2)) |
| 151 | + }) |
| 152 | +} |
| 153 | + |
| 154 | +access['ls-collaborators'] = access.lsCollaborators = ([pkg, usr], opts) => { |
| 155 | + return getPackage(pkg).then(pkgName => |
| 156 | + libaccess.lsCollaborators(pkgName, usr, opts) |
| 157 | + ).then(collabs => { |
| 158 | + // TODO - print these out nicely (breaking change) |
| 159 | + output(JSON.stringify(collabs, null, 2)) |
| 160 | + }) |
| 161 | +} |
| 162 | + |
| 163 | +function modifyPackage (pkg, opts, fn, requireScope = true) { |
| 164 | + return getPackage(pkg, requireScope).then(pkgName => |
| 165 | + otplease(opts, opts => fn(pkgName, opts)) |
| 166 | + ) |
| 167 | +} |
| 168 | + |
| 169 | +function getPackage (name, requireScope = true) { |
| 170 | + return BB.try(() => { |
| 171 | + if (name && name.trim()) { |
| 172 | + return name.trim() |
| 173 | + } else { |
| 174 | + return readPackageJson( |
| 175 | + path.resolve(prefix, 'package.json') |
| 176 | + ).then( |
| 177 | + data => data.name, |
| 178 | + err => { |
119 | 179 | if (err.code === 'ENOENT') {
|
120 |
| - cb(new Error('no package name passed to command and no package.json found')) |
| 180 | + throw new Error('no package name passed to command and no package.json found') |
121 | 181 | } else {
|
122 |
| - cb(err) |
| 182 | + throw err |
123 | 183 | }
|
124 |
| - } else { |
125 |
| - cb(null, data.name) |
126 | 184 | }
|
127 |
| - } |
128 |
| - ) |
129 |
| - } |
| 185 | + ) |
| 186 | + } |
| 187 | + }).then(name => { |
| 188 | + if (requireScope && !name.match(/^@[^/]+\/.*$/)) { |
| 189 | + UsageError('This command is only available for scoped packages.') |
| 190 | + } else { |
| 191 | + return name |
| 192 | + } |
| 193 | + }) |
130 | 194 | }
|
0 commit comments