Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
index.js279 linesDownload Raw Back to lib
1const { URL } = require('node:url')
2const timers = require('node:timers/promises')
3const fetch = require('npm-registry-fetch')
4const { HttpErrorBase } = require('npm-registry-fetch/lib/errors')
5const { log } = require('proc-log')
6
7// try loginWeb, catch the "not supported" message and fall back to couch
8const login = async (opener, prompter, opts = {}) => {
9  try {
10    return await loginWeb(opener, opts)
11  } catch (er) {
12    if (er instanceof WebLoginNotSupported) {
13      log.verbose('web login', 'not supported, trying couch')
14      const { username, password } = await prompter(opts.creds)
15      return loginCouch(username, password, opts)
16    }
17    throw er
18  }
19}
20
21const adduser = async (opener, prompter, opts = {}) => {
22  try {
23    return await adduserWeb(opener, opts)
24  } catch (er) {
25    if (er instanceof WebLoginNotSupported) {
26      log.verbose('web adduser', 'not supported, trying couch')
27      const { username, email, password } = await prompter(opts.creds)
28      return adduserCouch(username, email, password, opts)
29    }
30    throw er
31  }
32}
33
34const adduserWeb = (opener, opts = {}) => {
35  log.verbose('web adduser', 'before first POST')
36  return webAuth(opener, opts, { create: true })
37}
38
39const loginWeb = (opener, opts = {}) => {
40  log.verbose('web login', 'before first POST')
41  return webAuth(opener, opts, {})
42}
43
44const isValidUrl = u => {
45  try {
46    return /^https?:$/.test(new URL(u).protocol)
47  } catch {
48    return false
49  }
50}
51
52const webAuth = async (opener, opts, body) => {
53  try {
54    const res = await fetch('/-/v1/login', {
55      ...opts,
56      method: 'POST',
57      body,
58    })
59
60    const content = await res.json()
61    log.verbose('web auth', 'got response', content)
62
63    const { doneUrl, loginUrl } = content
64    if (!isValidUrl(doneUrl) || !isValidUrl(loginUrl)) {
65      throw new WebLoginInvalidResponse('POST', res, content)
66    }
67
68    return await webAuthOpener(opener, loginUrl, doneUrl, opts)
69  } catch (er) {
70    if ((er.statusCode >= 400 && er.statusCode <= 499) || er.statusCode === 500) {
71      throw new WebLoginNotSupported('POST', {
72        status: er.statusCode,
73        headers: er.headers,
74      }, er.body)
75    }
76    throw er
77  }
78}
79
80const webAuthOpener = async (opener, loginUrl, doneUrl, opts) => {
81  const abortController = new AbortController()
82  const { signal } = abortController
83  try {
84    log.verbose('web auth', 'opening url pair')
85    const [, authResult] = await Promise.all([
86      opener(loginUrl, { signal }).catch((err) => {
87        if (err.name === 'AbortError') {
88          abortController.abort()
89          return
90        }
91        throw err
92      }),
93      webAuthCheckLogin(doneUrl, { ...opts, cache: false }, { signal }).then((r) => {
94        log.verbose('web auth', 'done-check finished')
95        abortController.abort()
96        return r
97      }),
98    ])
99    return authResult
100  } catch (er) {
101    abortController.abort()
102    throw er
103  }
104}
105
106const webAuthCheckLogin = async (doneUrl, opts, { signal } = {}) => {
107  signal?.throwIfAborted()
108
109  const res = await fetch(doneUrl, opts)
110  const content = await res.json()
111
112  if (res.status === 200) {
113    if (!content.token) {
114      throw new WebLoginInvalidResponse('GET', res, content)
115    }
116    return content
117  }
118
119  if (res.status === 202) {
120    const retry = +res.headers.get('retry-after') * 1000
121    if (retry > 0) {
122      await timers.setTimeout(retry, null, { signal })
123    }
124    return webAuthCheckLogin(doneUrl, opts, { signal })
125  }
126
127  throw new WebLoginInvalidResponse('GET', res, content)
128}
129
130const couchEndpoint = (username) => `/-/user/org.couchdb.user:${encodeURIComponent(username)}`
131
132const putCouch = async (path, username, body, opts) => {
133  const result = await fetch.json(`${couchEndpoint(username)}${path}`, {
134    ...opts,
135    method: 'PUT',
136    body,
137  })
138  result.username = username
139  return result
140}
141
142const adduserCouch = async (username, email, password, opts = {}) => {
143  const body = {
144    _id: `org.couchdb.user:${username}`,
145    name: username,
146    password: password,
147    email: email,
148    type: 'user',
149    roles: [],
150    date: new Date().toISOString(),
151  }
152
153  log.verbose('adduser', 'before first PUT', {
154    ...body,
155    password: 'XXXXX',
156  })
157
158  return putCouch('', username, body, opts)
159}
160
161const loginCouch = async (username, password, opts = {}) => {
162  const body = {
163    _id: `org.couchdb.user:${username}`,
164    name: username,
165    password: password,
166    type: 'user',
167    roles: [],
168    date: new Date().toISOString(),
169  }
170
171  log.verbose('login', 'before first PUT', {
172    ...body,
173    password: 'XXXXX',
174  })
175
176  try {
177    return await putCouch('', username, body, opts)
178  } catch (err) {
179    if (err.code === 'E400') {
180      err.message = `There is no user with the username "${username}".`
181      throw err
182    }
183
184    if (err.code !== 'E409') {
185      throw err
186    }
187  }
188
189  const result = await fetch.json(couchEndpoint(username), {
190    ...opts,
191    query: { write: true },
192  })
193
194  for (const k of Object.keys(result)) {
195    if (!body[k] || k === 'roles') {
196      body[k] = result[k]
197    }
198  }
199
200  return putCouch(`/-rev/${body._rev}`, username, body, {
201    ...opts,
202    forceAuth: {
203      username,
204      password: Buffer.from(password, 'utf8').toString('base64'),
205      otp: opts.otp,
206    },
207  })
208}
209
210const get = (opts = {}) => fetch.json('/-/npm/v1/user', opts)
211
212const set = (profile, opts = {}) => fetch.json('/-/npm/v1/user', {
213  ...opts,
214  method: 'POST',
215  // profile keys can't be empty strings, but they CAN be null
216  body: Object.fromEntries(Object.entries(profile).map(([k, v]) => [k, v === '' ? null : v])),
217})
218
219const paginate = async (href, opts, items = []) => {
220  const result = await fetch.json(href, opts)
221  items = items.concat(result.objects)
222  if (result.urls.next) {
223    return paginate(result.urls.next, opts, items)
224  }
225  return items
226}
227
228const listTokens = (opts = {}) => paginate('/-/npm/v1/tokens', opts)
229
230const removeToken = async (tokenKey, opts = {}) => {
231  await fetch(`/-/npm/v1/tokens/token/${tokenKey}`, {
232    ...opts,
233    method: 'DELETE',
234    ignoreBody: true,
235  })
236  return null
237}
238
239const createToken = (password, readonly, cidrs, opts = {}) => fetch.json('/-/npm/v1/tokens', {
240  ...opts,
241  method: 'POST',
242  body: {
243    password: password,
244    readonly: readonly,
245    cidr_whitelist: cidrs,
246  },
247})
248
249class WebLoginInvalidResponse extends HttpErrorBase {
250  constructor (method, res, body) {
251    super(method, res, body)
252    this.message = 'Invalid response from web login endpoint'
253  }
254}
255
256class WebLoginNotSupported extends HttpErrorBase {
257  constructor (method, res, body) {
258    super(method, res, body)
259    this.message = 'Web login not supported'
260    this.code = 'ENYI'
261  }
262}
263
264module.exports = {
265  adduserCouch,
266  loginCouch,
267  adduserWeb,
268  loginWeb,
269  login,
270  adduser,
271  get,
272  set,
273  listTokens,
274  removeToken,
275  createToken,
276  webAuthCheckLogin,
277  webAuthOpener,
278}
279 
codekingpro/portable-devtools · Team Ai