edge-core-js
Version:
Edge account & wallet management library
171 lines (145 loc) • 4.3 kB
JavaScript
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }import { asMaybe } from 'cleaners'
import { base64 } from 'rfc4648'
import {
asChallengeErrorPayload,
asLoginResponseBody,
wasLoginRequestBody
} from '../../types/server-cleaners'
import {
ChallengeError,
NetworkError,
ObsoleteApiError,
OtpError,
PasswordError,
UsernameError
} from '../../types/types'
import { hmacSha256 } from '../../util/crypto/hashes'
import { utf8 } from '../../util/encoding'
import { timeout } from '../../util/promise'
export function parseReply(json) {
const clean = asLoginResponseBody(json)
switch (clean.status_code) {
case 0: // Success
return clean.results
case 2: // Account exists
throw new UsernameError('Account already exists on server')
case 3: // Account does not exist
throw new UsernameError('Account does not exist on server')
case 4: // Invalid password
case 5: // Invalid answers
throw new PasswordError(clean.results)
case 6: // Invalid API key
throw new Error('Invalid API key')
case 8: // Invalid OTP
throw new OtpError(clean.results)
case 1000: // Endpoint obsolete
throw new ObsoleteApiError()
case 1: // Error
case 7: // Pin expired
case 9: // Invalid voucher
case 10: // Conflicting change
case 11: // Rate limiting
default: {
const results = asMaybe(asChallengeErrorPayload)(clean.results)
if (results != null) {
throw new ChallengeError(results)
}
throw new Error(`Server error: ${clean.message}`)
}
}
}
/**
* Picks a random login server and makes a request.
*
* We don't use the normal async waterfall,
* since we never want these requests to happen in parallel.
* The first server needs to fully fail before we can try again,
* or we risk having document update conflicts, duplicated keys,
* redundant login notifications, or other corruption.
*/
export async function loginFetch(
ai,
method,
path,
body
) {
const { loginServers } = ai.props.state.login
// This will be out of range, but the modulo brings it back:
const startIndex = Math.floor(Math.random() * 255)
let response
let lastError = new Error('No login servers available')
for (let i = 0; i < loginServers.length; ++i) {
try {
const index = (startIndex + i) % loginServers.length
response = await loginFetchInner(
ai,
loginServers[index],
method,
path,
body
)
break
} catch (error) {
lastError = error
}
}
if (response == null) throw lastError
const { status } = response
const json = await response.json().catch(() => {
throw new Error(`Invalid reply JSON, HTTP status ${status}`)
})
return parseReply(json)
}
export function loginFetchInner(
ai,
serverUri,
method,
path,
body
) {
const { state, io, log } = ai.props
const { apiKey, apiSecret } = state.login
const bodyText =
method === 'GET' || body == null
? undefined
: JSON.stringify(wasLoginRequestBody(body))
// API key:
let authorization = `Token ${apiKey}`
if (apiSecret != null) {
const requestText = `${method}\n/api${path}\n${_nullishCoalesce(bodyText, () => ( ''))}`
const hash = hmacSha256(utf8.parse(requestText), apiSecret)
authorization = `HMAC ${apiKey} ${base64.stringify(hash)}`
}
const opts = {
body: bodyText,
method,
headers: {
'content-type': 'application/json',
accept: 'application/json',
authorization
},
corsBypass: 'never'
}
const start = Date.now()
const fullUri = `${serverUri}/api${path}`
return timeout(io.fetch(fullUri, opts), 30000).then(
response => {
// Log the results:
const time = Date.now() - start
log(`${method} ${fullUri} returned ${response.status} in ${time}ms`)
if (response.status === 409) {
log.crash(`Login API conflict error`, {
path
})
}
return response
},
networkError => {
const time = Date.now() - start
log.error(
`${method} ${fullUri} failed in ${time}ms, ${String(networkError)}`
)
throw new NetworkError(`Could not reach the auth server: ${path}`)
}
)
}