codekingpro/portable-devtools
114k
1const libnpmaccess = require('libnpmaccess')
2const npa = require('npm-package-arg')
3const { output } = require('proc-log')
4const pkgJson = require('@npmcli/package-json')
5const localeCompare = require('@isaacs/string-locale-compare')('en')
6const { otplease } = require('../utils/auth.js')
7const getIdentity = require('../utils/get-identity.js')
8const BaseCommand = require('../base-cmd.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 otplease(this.npm, this.npm.flatOptions, async (opts) => {
120 await libnpmaccess.setPermissions(scope, pkg, permissions, opts)
121 })
122 }
123
124 async #revoke (scope, pkg) {
125 await otplease(this.npm, this.npm.flatOptions, async (opts) => {
126 await libnpmaccess.removePermissions(scope, pkg, opts)
127 })
128 }
129
130 async #listPackages (owner, pkg) {
131 if (!owner) {
132 owner = await getIdentity(this.npm, this.npm.flatOptions)
133 }
134 const pkgs = await libnpmaccess.getPackages(owner, this.npm.flatOptions)
135 this.#output(pkgs, pkg)
136 }
137
138 async #listCollaborators (pkg, user) {
139 const pkgName = await this.#getPackage(pkg, false)
140 const collabs = await libnpmaccess.getCollaborators(pkgName, this.npm.flatOptions)
141 this.#output(collabs, user)
142 }
143
144 async #getStatus (pkg) {
145 const pkgName = await this.#getPackage(pkg, false)
146 const visibility = await libnpmaccess.getVisibility(pkgName, this.npm.flatOptions)
147 this.#output({ [pkgName]: visibility.public ? 'public' : 'private' })
148 }
149
150 async #set (subcmd, pkg) {
151 const [subkey, subval] = subcmd.split('=')
152 switch (subkey) {
153 case 'mfa':
154 case '2fa':
155 return this.#setMfa(pkg, subval)
156 case 'status':
157 return this.#setStatus(pkg, subval)
158 }
159 }
160
161 async #setMfa (pkg, level) {
162 const pkgName = await this.#getPackage(pkg, false)
163 await otplease(this.npm, this.npm.flatOptions, (opts) => {
164 return libnpmaccess.setMfa(pkgName, level, opts)
165 })
166 }
167
168 async #setStatus (pkg, status) {
169 // only scoped packages can have their access changed
170 const pkgName = await this.#getPackage(pkg, true)
171 if (status === 'private') {
172 status = 'restricted'
173 }
174 await otplease(this.npm, this.npm.flatOptions, (opts) => {
175 return libnpmaccess.setAccess(pkgName, status, opts)
176 })
177 return this.#getStatus(pkgName)
178 }
179
180 async #getPackage (name, requireScope) {
181 if (!name) {
182 try {
183 const { content } = await pkgJson.normalize(this.npm.prefix)
184 name = content.name
185 } catch (err) {
186 if (err.code === 'ENOENT') {
187 throw Object.assign(new Error('no package name given and no package.json found'), {
188 code: 'ENOENT',
189 })
190 } else {
191 throw err
192 }
193 }
194 }
195
196 const spec = npa(name)
197 if (requireScope && !spec.scope) {
198 throw this.usageError('This command is only available for scoped packages.')
199 }
200 return name
201 }
202
203 #output (items, limiter) {
204 const outputs = {}
205 const lookup = {
206 __proto__: null,
207 read: 'read-only',
208 write: 'read-write',
209 }
210 for (const item in items) {
211 const val = items[item]
212 outputs[item] = lookup[val] || val
213 }
214 if (this.npm.config.get('json')) {
215 output.buffer(outputs)
216 } else {
217 for (const item of Object.keys(outputs).sort(localeCompare)) {
218 if (!limiter || limiter === item) {
219 output.standard(`${item}: ${outputs[item]}`)
220 }
221 }
222 }
223 }
224}
225
226module.exports = Access
227 