Your IP : 3.145.64.210
const { URL } = require('node:url')
const timers = require('node:timers/promises')
const fetch = require('npm-registry-fetch')
const { HttpErrorBase } = require('npm-registry-fetch/lib/errors')
const { log } = require('proc-log')
// try loginWeb, catch the "not supported" message and fall back to couch
const login = async (opener, prompter, opts = {}) => {
try {
return await loginWeb(opener, opts)
} catch (er) {
if (er instanceof WebLoginNotSupported) {
log.verbose('web login', 'not supported, trying couch')
const { username, password } = await prompter(opts.creds)
return loginCouch(username, password, opts)
}
throw er
}
}
const adduser = async (opener, prompter, opts = {}) => {
try {
return await adduserWeb(opener, opts)
} catch (er) {
if (er instanceof WebLoginNotSupported) {
log.verbose('web adduser', 'not supported, trying couch')
const { username, email, password } = await prompter(opts.creds)
return adduserCouch(username, email, password, opts)
}
throw er
}
}
const adduserWeb = (opener, opts = {}) => {
log.verbose('web adduser', 'before first POST')
return webAuth(opener, opts, { create: true })
}
const loginWeb = (opener, opts = {}) => {
log.verbose('web login', 'before first POST')
return webAuth(opener, opts, {})
}
const isValidUrl = u => {
try {
return /^https?:$/.test(new URL(u).protocol)
} catch {
return false
}
}
const webAuth = async (opener, opts, body) => {
try {
const res = await fetch('/-/v1/login', {
...opts,
method: 'POST',
body,
})
const content = await res.json()
log.verbose('web auth', 'got response', content)
const { doneUrl, loginUrl } = content
if (!isValidUrl(doneUrl) || !isValidUrl(loginUrl)) {
throw new WebLoginInvalidResponse('POST', res, content)
}
return await webAuthOpener(opener, loginUrl, doneUrl, opts)
} catch (er) {
if ((er.statusCode >= 400 && er.statusCode <= 499) || er.statusCode === 500) {
throw new WebLoginNotSupported('POST', {
status: er.statusCode,
headers: er.headers,
}, er.body)
}
throw er
}
}
const webAuthOpener = async (opener, loginUrl, doneUrl, opts) => {
const abortController = new AbortController()
const { signal } = abortController
try {
log.verbose('web auth', 'opening url pair')
const [, authResult] = await Promise.all([
opener(loginUrl, { signal }).catch((err) => {
if (err.name === 'AbortError') {
abortController.abort()
return
}
throw err
}),
webAuthCheckLogin(doneUrl, { ...opts, cache: false }, { signal }).then((r) => {
log.verbose('web auth', 'done-check finished')
abortController.abort()
return r
}),
])
return authResult
} catch (er) {
abortController.abort()
throw er
}
}
const webAuthCheckLogin = async (doneUrl, opts, { signal } = {}) => {
signal?.throwIfAborted()
const res = await fetch(doneUrl, opts)
const content = await res.json()
if (res.status === 200) {
if (!content.token) {
throw new WebLoginInvalidResponse('GET', res, content)
}
return content
}
if (res.status === 202) {
const retry = +res.headers.get('retry-after') * 1000
if (retry > 0) {
await timers.setTimeout(retry, null, { ref: false, signal })
}
return webAuthCheckLogin(doneUrl, opts, { signal })
}
throw new WebLoginInvalidResponse('GET', res, content)
}
const couchEndpoint = (username) => `/-/user/org.couchdb.user:${encodeURIComponent(username)}`
const putCouch = async (path, username, body, opts) => {
const result = await fetch.json(`${couchEndpoint(username)}${path}`, {
...opts,
method: 'PUT',
body,
})
result.username = username
return result
}
const adduserCouch = async (username, email, password, opts = {}) => {
const body = {
_id: `org.couchdb.user:${username}`,
name: username,
password: password,
email: email,
type: 'user',
roles: [],
date: new Date().toISOString(),
}
log.verbose('adduser', 'before first PUT', {
...body,
password: 'XXXXX',
})
return putCouch('', username, body, opts)
}
const loginCouch = async (username, password, opts = {}) => {
const body = {
_id: `org.couchdb.user:${username}`,
name: username,
password: password,
type: 'user',
roles: [],
date: new Date().toISOString(),
}
log.verbose('login', 'before first PUT', {
...body,
password: 'XXXXX',
})
try {
return await putCouch('', username, body, opts)
} catch (err) {
if (err.code === 'E400') {
err.message = `There is no user with the username "${username}".`
throw err
}
if (err.code !== 'E409') {
throw err
}
}
const result = await fetch.json(couchEndpoint(username), {
...opts,
query: { write: true },
})
for (const k of Object.keys(result)) {
if (!body[k] || k === 'roles') {
body[k] = result[k]
}
}
return putCouch(`/-rev/${body._rev}`, username, body, {
...opts,
forceAuth: {
username,
password: Buffer.from(password, 'utf8').toString('base64'),
otp: opts.otp,
},
})
}
const get = (opts = {}) => fetch.json('/-/npm/v1/user', opts)
const set = (profile, opts = {}) => fetch.json('/-/npm/v1/user', {
...opts,
method: 'POST',
// profile keys can't be empty strings, but they CAN be null
body: Object.fromEntries(Object.entries(profile).map(([k, v]) => [k, v === '' ? null : v])),
})
const paginate = async (href, opts, items = []) => {
const result = await fetch.json(href, opts)
items = items.concat(result.objects)
if (result.urls.next) {
return paginate(result.urls.next, opts, items)
}
return items
}
const listTokens = (opts = {}) => paginate('/-/npm/v1/tokens', opts)
const removeToken = async (tokenKey, opts = {}) => {
await fetch(`/-/npm/v1/tokens/token/${tokenKey}`, {
...opts,
method: 'DELETE',
ignoreBody: true,
})
return null
}
const createToken = (password, readonly, cidrs, opts = {}) => fetch.json('/-/npm/v1/tokens', {
...opts,
method: 'POST',
body: {
password: password,
readonly: readonly,
cidr_whitelist: cidrs,
},
})
class WebLoginInvalidResponse extends HttpErrorBase {
constructor (method, res, body) {
super(method, res, body)
this.message = 'Invalid response from web login endpoint'
}
}
class WebLoginNotSupported extends HttpErrorBase {
constructor (method, res, body) {
super(method, res, body)
this.message = 'Web login not supported'
this.code = 'ENYI'
}
}
module.exports = {
adduserCouch,
loginCouch,
adduserWeb,
loginWeb,
login,
adduser,
get,
set,
listTokens,
removeToken,
createToken,
webAuthCheckLogin,
webAuthOpener,
}