Comparing version 1.2.1 to 2.0.0
@@ -1,10 +0,9 @@ | ||
if (!process.env.MYTHX_ETH_ADDRESS && !process.env.MYTHX_EMAIL) { | ||
console.log('Please set either environment variable MYTHX_ETH_ADDRESS ' + | ||
'or MYTHX_EMAIL') | ||
if (!process.env.MYTHX_ETH_ADDRESS) { | ||
console.log('Please set either environment variable MYTHX_ETH_ADDRESS ') | ||
process.exit(2) | ||
} | ||
if (!process.env.MYTHX_PASSWORD && !process.env.MYTHX_API_KEY) { | ||
console.log('Please set environment variable MYTHX_PASSWORD or MYTHX_API_KEY') | ||
if (!process.env.MYTHX_PASSWORD) { | ||
console.log('Please set environment variable MYTHX_PASSWORD') | ||
process.exit(3) | ||
} |
@@ -1,3 +0,3 @@ | ||
Here we have some minimal command-line nodejs that can be run to show | ||
how to use armlet and interact with the MythX API. | ||
Here we have some command-line nodejs that can be run to show how to | ||
use armlet and interact with the MythX API. | ||
@@ -10,5 +10,5 @@ See the [openapi spec](https://api.mythx.io/v1/openapi) for details of the MythX API. | ||
* [analysis](https://github.com/ConsenSys/armlet/blob/master/example/analysis): Submit a JSON with Solidity source and EVM bytecode information to the MythX and retrieve results. | ||
* [analysis-status](https://github.com/ConsenSys/armlet/blob/master/example/analysis): Get status of a prior MythX analysis request. | ||
* [analysis-issues](https://github.com/ConsenSys/armlet/blob/master/example/analysis): Get issues reported from a prior MythX analysis. | ||
* [mythx-analysis](https://github.com/ConsenSys/armlet/blob/master/example/mythx-analysis): Submit a JSON with Solidity source and EVM bytecode information to the MythX and retrieve results. | ||
* [analysis-status](https://github.com/ConsenSys/armlet/blob/master/example/analysis-status): Get status of a prior MythX analysis request. | ||
* [analysis-issues](https://github.com/ConsenSys/armlet/blob/master/example/analysis-issues): Get issues reported from a prior MythX analysis. | ||
* [list-analyses](https://github.com/ConsenSys/armlet/blob/master/example/list-analyses): Get issues reported from a prior MythX analysis. | ||
@@ -15,0 +15,0 @@ * [api-version](https://github.com/ConsenSys/armlet/blob/master/example/api-version): Retrieve MythX API version information. JSON is output. |
{ | ||
"contractName": "PublicStorageArray", | ||
"contractName": "PublicStorageArray14", | ||
"abi": [ | ||
@@ -4,0 +4,0 @@ { |
162
index.js
@@ -5,10 +5,15 @@ const url = require('url') | ||
const simpleRequester = require('./lib/simpleRequester') | ||
const poller = require('./lib/poller') | ||
const analysisPoller = require('./lib/analysisPoller') | ||
const login = require('./lib/login') | ||
const refresh = require('./lib/refresh') | ||
const libUtil = require('./lib/util') | ||
const defaultApiUrl = process.env['MYTHX_API_URL'] || 'https://api.mythx.io' | ||
const defaultApiVersion = 'v1' | ||
const trialUserId = '123456789012345678901234' | ||
// No MythX job we've seen is faster than this value. So if an | ||
// analysis request isn't cached, then the *first* poll for status | ||
// will be delayed by this amount of time. | ||
const defaultInitialDelay = 45000 // 45 seconds | ||
class Client { | ||
@@ -21,4 +26,3 @@ /** | ||
* @param {auth} object - login or authentication information which contains | ||
* (email | ethAddress) and a password or... | ||
* apiKey | ||
* an ethAddress and a password | ||
* @param {inputApiUrl} string - Optional. A URL of a MythX API server we want to contect | ||
@@ -29,17 +33,8 @@ * to. | ||
constructor (auth, inputApiUrl = defaultApiUrl) { | ||
const { email, ethAddress, apiKey, password } = auth || {} | ||
const { ethAddress, password } = auth || {} | ||
let userId | ||
if (!password && !email && !ethAddress && !apiKey) { | ||
userId = trialUserId | ||
if (!password || !ethAddress) { | ||
throw new TypeError('Please provide an Ethereum address and a password.') | ||
} | ||
if (password && !email && !ethAddress && !apiKey) { | ||
throw new TypeError('Please provide an user id auth option.') | ||
} | ||
if (!apiKey && !userId && (!password && (email || ethAddress))) { | ||
throw new TypeError('Please provide a password auth option.') | ||
} | ||
const apiUrl = new url.URL(inputApiUrl) | ||
@@ -50,7 +45,4 @@ if (!apiUrl.hostname) { | ||
this.userId = userId | ||
this.email = email | ||
this.ethAddress = ethAddress | ||
this.password = password | ||
this.accessToken = apiKey | ||
this.apiUrl = apiUrl | ||
@@ -63,6 +55,13 @@ } | ||
* | ||
* @param {options} object - structure which must contain | ||
* {data} object - information containing Smart Contract information to be analyzed | ||
* {timeout} number - optional timeout value in milliseconds | ||
* @param {options} object - structure which must contain | ||
* {data} object - information containing Smart Contract information to be analyzed | ||
* {timeout} number - optional timeout value in milliseconds | ||
* {clientToolName} string - optional; sets up for client tool usage tracking | ||
* {initialDelay} number - optional; After submitting an analysis and seeing that it is | ||
not cached, the first status API call will be delayed by this | ||
number of milliseconds | ||
minimum value for how long a non-cached analyses will take | ||
* this must be larger than defaultInitialDelay which we believe to be | ||
* the smallest reasonable value. | ||
* | ||
@@ -76,3 +75,3 @@ * @returns an array-like object of issues, and a uuid attribute which can | ||
if (options === undefined || options.data === undefined) { | ||
throw new TypeError('Please provide a data option.') | ||
throw new TypeError('Please provide analysis request JSON in a "data" attribute.') | ||
} | ||
@@ -83,8 +82,6 @@ | ||
try { | ||
tokens = await login.do(this.email, this.ethAddress, this.userId, this.password, this.apiUrl) | ||
tokens = await login.do(this.ethAddress, this.password, this.apiUrl) | ||
} catch (e) { | ||
let authType = '' | ||
if (this.email) { | ||
authType = ` for email address ${this.email}` | ||
} else if (this.ethAddress) { | ||
if (this.ethAddress) { | ||
authType = ` for ethereum address ${this.ethAddress}` | ||
@@ -99,5 +96,5 @@ } | ||
let uuid | ||
let requestResponse | ||
try { | ||
uuid = await requester.do(options, this.accessToken, this.apiUrl) | ||
requestResponse = await requester.do(options, this.accessToken, this.apiUrl) | ||
} catch (e) { | ||
@@ -111,20 +108,58 @@ if (e.statusCode !== 401) { | ||
uuid = await requester.do(options, this.accessToken, this.apiUrl) | ||
requestResponse = await requester.do(options, this.accessToken, this.apiUrl) | ||
} | ||
/* | ||
Set "timeout" - the maximum amount of time we want to wait on | ||
a request before giving up. | ||
Unless a timeout has been explicitly given (and we recommend it should be), | ||
we will use a value of 3 minutes for a "quick" analysis and | ||
3 hours for a "full" analysis. | ||
Note: | ||
A "quick" analysis usually finishes within 90 seconds after the job starts. | ||
A "full" analysis may run for 2 hours or more. | ||
There is also average queuing delay as well which on average may be at | ||
least 5 seconds. | ||
*/ | ||
let timeout = options.timeout | ||
if (!('timeout' in options)) { | ||
options.timeout = (60 * 1000) * (options.data.analysisMode === 'full') | ||
? 3 // 3 minutes | ||
: (3 * 60) // 3 hours | ||
} | ||
if (options.debug) { | ||
console.log(`now: ${Math.trunc(Date.now() / 1000)}`) | ||
} | ||
let result | ||
try { | ||
result = await poller.do(uuid, this.accessToken, this.apiUrl, undefined, options.timeout) | ||
} catch (e) { | ||
if (e.statusCode !== 401) { | ||
throw e | ||
if (requestResponse.status === 'Finished') { | ||
result = await analysisPoller.getIssues(requestResponse.uuid, this.accessToken, this.apiUrl) | ||
if (options.debug) { | ||
const util = require('util') | ||
let depth = (options.debug > 1) ? 10 : 2 | ||
console.log(`Cached Result:\n${util.inspect(result, { depth: depth })}\n------`) | ||
} | ||
const tokens = await refresh.do(this.accessToken, this.refreshToken, this.apiUrl) | ||
this.accessToken = tokens.access | ||
this.refreshToken = tokens.refresh | ||
} else { | ||
const initialDelay = Math.max(options.initialDelay || 0, defaultInitialDelay) | ||
try { | ||
result = await analysisPoller.do(requestResponse.uuid, this.accessToken, this.apiUrl, timeout, initialDelay, options.debug) | ||
} catch (e) { | ||
if (e.statusCode !== 401) { | ||
throw e | ||
} | ||
const tokens = await refresh.do(this.accessToken, this.refreshToken, this.apiUrl) | ||
this.accessToken = tokens.access | ||
this.refreshToken = tokens.refresh | ||
result = await analysisPoller.do(requestResponse.uuid, this.accessToken, this.apiUrl, timeout, | ||
initialDelay, options.debug) | ||
} | ||
} | ||
result = await poller.do(uuid, this.accessToken, this.apiUrl, undefined, options.timeout) | ||
return { | ||
issues: result, | ||
uuid: requestResponse.uuid | ||
} | ||
result.uuid = uuid | ||
return result | ||
} | ||
@@ -148,7 +183,7 @@ | ||
if (!this.accessToken) { | ||
const tokens = await login.do(this.email, this.ethAddress, this.userId, this.password, this.apiUrl) | ||
const tokens = await login.do(this.ethAddress, this.password, this.apiUrl) | ||
this.accessToken = tokens.access | ||
this.refreshToken = tokens.refresh | ||
} | ||
const url = `${this.apiUrl.href}${defaultApiVersion}/analyses?dateFrom=${options.dateFrom}&dateTo=${options.dateTo}&offset=${options.offset}` | ||
const url = libUtil.joinUrl(this.apiUrl.href, `${defaultApiVersion}/analyses?dateFrom=${options.dateFrom}&dateTo=${options.dateTo}&offset=${options.offset}`) | ||
let analyses | ||
@@ -173,18 +208,20 @@ try { | ||
* | ||
* @param {options} object - structure which must contain: | ||
* @param {object} options - structure which must contain: | ||
* {data} object - information containing Smart Contract information to be analyzed | ||
* {timeout} number - optional timeout value in milliseconds | ||
* {clientToolName} string - optional; sets up for client tool usage tracking | ||
* {Number} timeout - optional timeout value in milliseconds | ||
* {String} clientToolName - optional; sets up for client tool usage tracking | ||
* | ||
* @returns object which contains: | ||
* (issues} object - an like object which of issues is grouped by (file) input container. | ||
* {status} object - status information as returned in each object of analyses(). | ||
* (Number} elsped - elaped milliseconds that we recorded | ||
* (Object} issues - an like object which of issues is grouped by (file) input container. | ||
* {Object} status - status information as returned in each object of analyses(). | ||
* | ||
**/ | ||
async analyzeWithStatus (options) { | ||
const issues = await this.analyze(options) | ||
const uuid = issues.uuid | ||
delete issues.uuid | ||
const start = Date.now() | ||
const { issues, uuid } = await this.analyze(options, true) | ||
const status = await this.getStatus(uuid) | ||
const elapsed = Date.now() - start | ||
return { | ||
elapsed, | ||
issues, | ||
@@ -198,3 +235,3 @@ status | ||
if (!accessToken) { | ||
const tokens = await login.do(this.email, this.ethAddress, this.userId, this.password, this.apiUrl) | ||
const tokens = await login.do(this.ethAddress, this.password, this.apiUrl) | ||
accessToken = tokens.access | ||
@@ -220,3 +257,3 @@ } | ||
async getStatus (uuid, inputApiUrl = defaultApiUrl) { | ||
const url = `${inputApiUrl}/${defaultApiVersion}/analyses/${uuid}` | ||
const url = libUtil.joinUrl(this.apiUrl.href, `${defaultApiVersion}/analyses/${uuid}`) | ||
return this.getStatusOrIssues(uuid, url, inputApiUrl) | ||
@@ -226,3 +263,3 @@ } | ||
async getIssues (uuid, inputApiUrl = defaultApiUrl) { | ||
const url = `${inputApiUrl}/${defaultApiVersion}/analyses/${uuid}/issues` | ||
const url = libUtil.joinUrl(this.apiUrl.href, `${defaultApiVersion}/analyses/${uuid}/issues`) | ||
return this.getStatusOrIssues(uuid, url, inputApiUrl) | ||
@@ -234,7 +271,10 @@ } | ||
if (!accessToken) { | ||
const tokens = await login.do(this.email, this.ethAddress, this.userId, this.password, this.apiUrl) | ||
const tokens = await login.do(this.ethAddress, this.password, this.apiUrl) | ||
accessToken = tokens.access | ||
} | ||
const url = `${inputApiUrl}/${defaultApiVersion}/analyses` | ||
return simpleRequester.do({ url, accessToken: accessToken, json: true }) | ||
const url = libUtil.joinUrl(inputApiUrl, `${defaultApiVersion}/analyses`) | ||
return simpleRequester.do({ url, | ||
accessToken: accessToken, | ||
json: true, | ||
ethAddress: this.ethAddress }) | ||
} | ||
@@ -244,7 +284,9 @@ } | ||
module.exports.ApiVersion = (inputApiUrl = defaultApiUrl) => { | ||
return simpleRequester.do({ url: `${inputApiUrl}/${defaultApiVersion}/version`, json: true }) | ||
const url = libUtil.joinUrl(inputApiUrl, `${defaultApiVersion}/version`) | ||
return simpleRequester.do({ url, json: true }) | ||
} | ||
module.exports.OpenApiSpec = (inputApiUrl = defaultApiUrl) => { | ||
return simpleRequester.do({ url: `${inputApiUrl}/${defaultApiVersion}/openapi.yaml` }) | ||
const url = libUtil.joinUrl(inputApiUrl, `${defaultApiVersion}/openapi.yaml`) | ||
return simpleRequester.do({ url }) | ||
} | ||
@@ -256,2 +298,2 @@ | ||
module.exports.defaultApiVersion = defaultApiVersion | ||
module.exports.trialUserId = trialUserId | ||
module.exports.defaultInitialDelay = defaultInitialDelay |
const request = require('request') | ||
const util = require('./util') | ||
const basePath = 'v1/auth/login' | ||
exports.do = (email, ethAddress, userId, password, apiUrl) => { | ||
exports.do = (ethAddress, password, apiUrl) => { | ||
return new Promise((resolve, reject) => { | ||
const options = { form: { email, userId, ethAddress, password } } | ||
const url = `${apiUrl.href}${basePath}` | ||
const options = { form: { ethAddress, password } } | ||
const url = util.joinUrl(apiUrl.href, basePath) | ||
@@ -16,4 +17,22 @@ request.post(url, options, (error, res, body) => { | ||
// Handle redirect | ||
if (res.statusCode === 308 && apiUrl.protocol === 'http:') { | ||
apiUrl.protocol = 'https:' | ||
this.do(ethAddress, password, apiUrl).then(result => { | ||
resolve(result) | ||
}).catch(error => { | ||
reject(error) | ||
}) | ||
return | ||
} | ||
if (res.statusCode !== 200) { | ||
reject(new Error(`Invalid status code ${res.statusCode}, ${body}`)) | ||
/* eslint-disable prefer-promise-reject-errors */ | ||
try { | ||
body = JSON.parse(body) | ||
reject(`${body.error} (HTTP status ${res.statusCode})`) | ||
} catch (err) { | ||
reject(`${body} (HTTP status ${res.statusCode})`) | ||
} | ||
/* eslint-enable prefer-promise-reject-errors */ | ||
return | ||
@@ -20,0 +39,0 @@ } |
const request = require('request') | ||
const util = require('./util') | ||
@@ -8,3 +9,3 @@ const basePath = 'v1/auth/refresh' | ||
const options = { form: { refreshToken, accessToken } } | ||
const url = `${apiUrl.href}${basePath}` | ||
const url = util.joinUrl(apiUrl.href, basePath) | ||
@@ -11,0 +12,0 @@ request.post(url, options, (error, res, body) => { |
const request = require('request') | ||
const util = require('./util') | ||
const basePath = 'v1/analyses' | ||
@@ -9,3 +10,3 @@ | ||
const options = { | ||
url: `${apiUrl.href}${basePath}`, | ||
url: util.joinUrl(apiUrl.href, basePath), | ||
method: 'POST', | ||
@@ -45,11 +46,13 @@ headers: { | ||
}, []) | ||
reject(new Error(`${errMsg}: ${msgs.join(', ')}`)) | ||
// eslint-disable-next-line prefer-promise-reject-errors | ||
reject(`${errMsg}: ${msgs.join(', ')}`) | ||
return | ||
} | ||
if (typeof data !== 'object') { | ||
reject(new SyntaxError('Non JSON data returned')) | ||
// eslint-disable-next-line prefer-promise-reject-errors | ||
reject(`Non JSON data returned: ${data}`) | ||
} | ||
resolve(data.uuid) | ||
resolve(data) | ||
}) | ||
}) | ||
} |
@@ -0,3 +1,12 @@ | ||
/* | ||
This is somewhat generic but specific to the MythX API and handles | ||
API requests which do not require more than a single request. | ||
This is in contrast, say, to analysis where a request, needs | ||
to be followed by polling. | ||
*/ | ||
const request = require('request') | ||
const HttpErrors = require('http-errors') | ||
const trialEthAccount = '0x0000000000000000000000000000000000000000' | ||
@@ -27,2 +36,8 @@ exports.do = options => { | ||
return | ||
} else if ((res.statusCode === 403) && | ||
(res.request.uri.path.endsWith('/analyses')) && | ||
(options.ethAddress === trialEthAccount)) { | ||
// FIXME: Remove when API gives back a custom message like this: | ||
// eslint-disable-next-line prefer-promise-reject-errors | ||
reject('The trial user does not allow listing previous analyses. To enable this feature, please sign up for a free account on: https://mythx.io') | ||
} else if (res.statusCode !== 200) { | ||
@@ -29,0 +44,0 @@ try { |
83
NEWS.md
@@ -0,1 +1,84 @@ | ||
Release 2.0.0 | ||
================= | ||
A lot has changed in the almost two weeks that have | ||
elapsed since the last release. | ||
Changes for Mythx API 1.4 | ||
------------------------------- | ||
Perhaps the biggest change is that we now support version 1.4.0 of the MythX API. | ||
This means various authentication options involving an API key or an email address | ||
are no longer supported. | ||
There were some smaller changes in the back end and the | ||
acceptable way to interact with the back-end protocol has been adjusted. | ||
Geometrically-increasing delays in polling | ||
----------------------------------------------------- | ||
We noticed that there was a lot of overhead created on the back end caused by | ||
polling for analysis status. Taking a cue from how the Ethernet | ||
handles congestion, successive polls are now spaced more widely. | ||
For applications which use MythX through armlet, when they can predict | ||
the likely time interval for the contract submitted, they will be | ||
rewarded with a reduced delay in noticing that results are ready on the back-end. | ||
Parameter `initial-delay` was added. This is the minimum amount of | ||
time that this library waits before attempting its first status poll | ||
when the results are not already cached. | ||
You can read about improving polling response [here](https://github.com/ConsenSys/armlet/#improving-polling-response). | ||
Introducing command-line utility "mythx-analysis" | ||
------------------------------------------------------------- | ||
The "example" program `analysis` is now called `mythx-analysis` and it | ||
is installed as a standalone command-line utility. | ||
It is more full featured: | ||
* it supports more armlet library options, | ||
`--version`, `--timeout`, `--delay`, and `--debug` | ||
* it can accept Solidity source code and will run `solc` to compile the source before passing | ||
on to MythX | ||
Sample Solidity contracts now appear in `example/solidity-files` | ||
Library changes not mentioned above | ||
-------------------------------------------- | ||
An additional analysis option `debug` is available. With this, you | ||
can get more information about what is going on in armlet. Setting | ||
`debug` to a numeric value of 2 or more gives more-verbose | ||
output. | ||
Getting a list of past analyses is not allowed as a trial user, as | ||
is now noted in the response. We suggest a suitable course of action | ||
(registering) and supply a link to do so. | ||
Some small URL canonicalization is now done. In particular you can add a trailing slash | ||
to the HTTP host `https://api.mythix.io/` and that is the same things as `https://api.mythix.io`. | ||
Similarly `http` will be turned into `https` when appropriate. | ||
There is now proxy support via | ||
[`omni-fetch`](https://www.npmjs.com/package/omni-fetch) which is a | ||
wrapper to | ||
[`isomorphic-fetch`](https://www.npmjs.com/package/isomorphic-fetch). This | ||
work was kindly contributed by Teruhiro Tagomori at NRISecure. | ||
Additional tests were added and test-code coverage has been | ||
increased. This is the work of Daniyar Chambylov at Maddevs. | ||
Some time units are shown in a more human-friendly way. There are numerous other small documentation and code improvements. | ||
Older Releases | ||
================= | ||
v1.2.1 - 2019-02-06 | ||
@@ -2,0 +85,0 @@ ----------------------- |
{ | ||
"name": "armlet", | ||
"version": "1.2.1", | ||
"description": "A MythX API client.", | ||
"version": "2.0.0", | ||
"description": [ | ||
"Armlet is a thin wrapper around the MythX API written in Javascript.", | ||
"It simplifies interaction with MythX. For example, the library", | ||
"wraps API analysis requests into a promise. A MythX API client.", | ||
"", | ||
"A simple command-line tool, mythx-analysis, is provided to show how to", | ||
"use the API. It can be used to run MythX analyses on a single Solidity", | ||
"smart-contract text file." | ||
], | ||
"main": "index.js", | ||
"bin": { | ||
"mythx-analysis": "./example/mythx-analysis" | ||
}, | ||
"directories": { | ||
"example": "example", | ||
"lib": "lib" | ||
}, | ||
"scripts": { | ||
@@ -17,3 +32,2 @@ "lint": "eslint .", | ||
}, | ||
"author": "Federico Gimenez <federico.gimenez@gmail.com>", | ||
"license": "MIT", | ||
@@ -25,3 +39,2 @@ "bugs": { | ||
"devDependencies": { | ||
"chai": "^4.1.2", | ||
"chai-as-promised": "^7.1.1", | ||
@@ -43,5 +56,6 @@ "coveralls": "^3.0.2", | ||
"humanize-duration": "^3.17.0", | ||
"moment": "^2.24.0", | ||
"isomorphic-fetch": "^2.2.1", | ||
"omni-fetch": "^0.2.3", | ||
"request": "^2.88.0" | ||
} | ||
} |
125
README.md
@@ -6,14 +6,28 @@ [![CircleCI](https://circleci.com/gh/ConsenSys/armlet.svg?style=svg)](https://circleci.com/gh/ConsenSys/armlet) | ||
Armlet is a thin wrapper around the MythX API written in Javascript | ||
which simplifies interaction with MythX. For example, the library | ||
wraps API analysis requests into a promise. | ||
Armlet is a thin wrapper around the MythX API written in Javascript. | ||
It simplifies interaction with MythX. For example, the library | ||
wraps API analysis requests into a promise, merges status information | ||
with analysis-result information, and judiciously polls for results. | ||
A simple command-line tool, `mythx-analysis`, is provided to show how to use the API. | ||
It can be used to run MythX analyses on a single Solidity smart-contract text file.", | ||
# Installation | ||
Just as with any nodejs package, install with: | ||
To install the latest stable version from NPM: | ||
``` | ||
$ npm install armlet | ||
$ npm -g install armlet | ||
``` | ||
If you're feeling adventurous, you can also install the from the master branch: | ||
``` | ||
$ npm install -g git+https://git@github.com/ConsenSys/armlet.git | ||
``` | ||
The `-g` or `--global` option above may not be needed depending on how | ||
you work. It my ensuring `mythx-analysis` is in your path where it might not | ||
otherwise be there. | ||
# Example | ||
@@ -40,3 +54,3 @@ | ||
{ | ||
password: process.env.MYTHX_PASSWORD, // adjust this | ||
password: process.env.MYTHX_PASSWORD, | ||
ethAddress: process.env.MYTHX_ETH_ADDRESS, | ||
@@ -49,3 +63,8 @@ }) | ||
client.analyzeWithStatus({data}) | ||
client.analyzeWithStatus( | ||
{ | ||
"data": data, // required | ||
"timeout": 2 * 60 * 1000, // optional, but can improve response time | ||
"debug": false, // optional: set to true if you want to see what's going on | ||
}) | ||
.then(result => { | ||
@@ -59,10 +78,12 @@ const util = require('util'); | ||
``` | ||
You can also specify the timeout in milliseconds to wait for the analysis to be | ||
done (the default is 40 seconds). Also, for statistical tracking you can tag the type of tool making the request using `clientToolName`. | ||
For statistical tracking you can tag the type of tool making the request using `clientToolName`. | ||
For example, to log analysis request as a use of `armlet-readme`, run: | ||
As an example, to wait up to 50 seconds, and log analysis request as as use of `armlet-readme`, run: | ||
```javascript | ||
client.analyzeWithStatus({data, timeout: 50000, clientToolName: 'armlet-readme'}) | ||
client.analyzeWithStatus( | ||
{ | ||
"data": data, | ||
"clientToolName": "armlet-readme" | ||
}) | ||
.then(result => { | ||
@@ -76,2 +97,82 @@ console.log(result.status, {depth: null}) | ||
# Improving Polling Response | ||
There are two time parameters, given in milliseconds, that change how quickly a analysis result is reported back: | ||
* initial delay | ||
* maximum delay | ||
The initial delay is the minimum amount of time that this library | ||
waits before attempting its first status poll. Note however that if a | ||
request has been cached, then results come back immediately and no | ||
status polling is needed. (The server caches previous analysis runs; | ||
it takes into account the data passed to it, the analysis mode, and the | ||
back-end versions of components used to provide the analysis.) | ||
The maximum delay is the maximum amount of time we will wait for an | ||
analysis to complete. Note, however, that if the processing has not | ||
finished when this timeout is reached, it may still be running on the | ||
server side. Therefore when a timeout occurs, you will get back a | ||
UUID which can subsequently be used to get status and results. | ||
The closer these two parameters are to the actual time range that is | ||
needed by analysis, the faster the response will get reported back | ||
after completion on the server end. Below we explain | ||
* why we have these two parameters, | ||
* why giving good guesses helps response in reporting results, | ||
* how you can get good guesses. | ||
Until we have a websocket interface so the server can directly | ||
pass back results without any additional action required on the server | ||
side, your REST API requires the client to poll for status. We have | ||
seen that this polling can cause a lot of overhead, if not done | ||
judiciously. So, each request is allowed up to 10 status probes. | ||
We have seen that _no_ analysis request will finish in less than a | ||
certain period of time. Since the number of probe per analysis is | ||
limited, it doesn't make sense to probe before the fastest | ||
analysis-completion time. | ||
The 10 status probes are done in geometrically increasing time | ||
intervals. The first interval is the shortest and the last interval is | ||
the longest. The response rate at the beginning is better than the | ||
response rate at the end, in terms of how much additional time it | ||
takes before the analysis completion is noticed. | ||
However this progression is not fixed. Instead, it takes into account | ||
the maximum amount of time you are willing to wait for a result. | ||
In other words, the shorter the short period of time you give for the | ||
maximum timeout, the shorter the geometric succession of the 10 probes | ||
allotted to an analysis request will be. | ||
To make this clear, if you only want to wait a; maximum of two minutes, then | ||
the first delay will be 0.3 seconds, while the delay before last poll | ||
will be about half a minute. If on the other hand you want to wait up | ||
to 2 hours, then the first delay will be 9 seconds, and the last one will | ||
be about 15 minutes. | ||
Good guessing of these two parameters reduces the | ||
unnecessary probe time while providing good response around the declared | ||
period of time around that was declared. | ||
So, how can you guess decent values? We have reasonable defaults built | ||
in. But there are two factors that you can use to get better estimates. | ||
The first is the kind of analysis mode used: a "quick" analysis will | ||
usually be under two minutes, while a "full" analysis will usually be | ||
under two hours. | ||
When an analysis request finishes, we provide the amount of time used | ||
broken into two components: the amount of time spent in analysis, and | ||
the amount of time spent in queuing. The queuing time can vary | ||
depending on what else is going on when the analysis request | ||
was sent, so that's why it is separated out. In addition, the | ||
library provides its own elapsed time in the response. | ||
If you are making an analysis within an IDE which saves reports of | ||
past runs, such as truffle or VSCode, the timings can be used for | ||
estimates. | ||
# See Also | ||
@@ -78,0 +179,0 @@ |
@@ -12,7 +12,6 @@ const armlet = require('../index') | ||
const simpleRequester = require('../lib/simpleRequester') | ||
const poller = require('../lib/poller') | ||
const poller = require('../lib/analysisPoller') | ||
const login = require('../lib/login') | ||
const refresh = require('../lib/refresh') | ||
const email = 'user@example.com' | ||
const ethAddress = '0x74B904af705Eb2D5a6CDc174c08147bED478a60d' | ||
@@ -48,26 +47,16 @@ const password = 'my-password' | ||
describe('should have a constructor which should', () => { | ||
it('initialize with trial userId', () => { | ||
const instance = new Client() | ||
instance.userId.should.be.deep.equal(armlet.trialUserId) | ||
it('throw error when initialize with no auth parameters', () => { | ||
(() => new Client()).should.throw(TypeError, /Please provide/) | ||
}) | ||
it('require a password auth option if email is provided', () => { | ||
(() => new Client({ email })).should.throw(TypeError) | ||
it('require an ethAddress and password ', () => { | ||
(() => new Client({ ethAddress })).should.throw(TypeError, /Please provide/) | ||
}) | ||
it('require a password auth option if ethAddress is provided', () => { | ||
(() => new Client({ ethAddress })).should.throw(TypeError) | ||
}) | ||
it('require an user id auth option', () => { | ||
(() => new Client({ password })).should.throw(TypeError) | ||
}) | ||
it('require a valid apiUrl if given', () => { | ||
(() => new Client({ email, password }, 'not-a-valid-url')).should.throw(TypeError) | ||
(() => new Client({ ethAddress, password }, 'not-a-valid-url')).should.throw(TypeError) | ||
}) | ||
it('initialize apiUrl to a default value if not given', () => { | ||
const instance = new Client({ email, password }) | ||
const instance = new Client({ ethAddress, password }) | ||
@@ -78,3 +67,3 @@ instance.apiUrl.should.be.deep.equal(armlet.defaultApiUrl) | ||
it('initialize apiUrl to the given value', () => { | ||
const instance = new Client({ email, password }, apiUrl) | ||
const instance = new Client({ ethAddress, password }, apiUrl) | ||
@@ -84,11 +73,5 @@ instance.apiUrl.should.be.deep.equal(new url.URL(apiUrl)) | ||
it('accept an apiKey auth and store it as accessToken', () => { | ||
const instance = new Client({ apiKey: 'my-apikey' }) | ||
instance.accessToken.should.be.equal('my-apikey') | ||
}) | ||
describe('instances should', () => { | ||
beforeEach(() => { | ||
this.instance = new Client({ email, password }) | ||
this.instance = new Client({ ethAddress, password }) | ||
}) | ||
@@ -125,14 +108,24 @@ | ||
const input = { data: 'content' } | ||
const analyzeStub = sinon.stub(this.instance, 'analyze') | ||
sinon.stub(Date, 'now') | ||
.returns(1) | ||
sinon.stub(this.instance, 'analyze') | ||
.withArgs(input, true) | ||
.resolves({ issues: 'issues', uuid: 'uuid' }) | ||
const getStatusStub = sinon.stub(this.instance, 'getStatus') | ||
sinon.stub(this.instance, 'getStatus') | ||
.withArgs('uuid') | ||
.resolves('stubbed') | ||
await this.instance.analyzeWithStatus(input) | ||
.should.eventually.deep.equal({ issues: { issues: 'issues' }, status: 'stubbed' }) | ||
analyzeStub.calledWith(input).should.be.equal(true) | ||
getStatusStub.calledWith('uuid').should.be.equal(true) | ||
.should.eventually.deep.equal({ | ||
elapsed: 0, | ||
issues: 'issues', | ||
status: 'stubbed' | ||
}) | ||
analyzeStub.restore() | ||
getStatusStub.restore() | ||
this.instance.analyze.restore() | ||
this.instance.getStatus.restore() | ||
Date.now.restore() | ||
}) | ||
@@ -185,3 +178,3 @@ }) | ||
beforeEach(() => { | ||
this.instance = new Client({ email, ethAddress, password }, apiUrl) | ||
this.instance = new Client({ ethAddress, password }, apiUrl) | ||
}) | ||
@@ -198,3 +191,3 @@ | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
@@ -208,3 +201,3 @@ resolve({ access: accessToken, refresh: refreshToken }) | ||
})) | ||
// await this.instance.getStatus(uuid).should.eventually.equal('stubbed') | ||
await this.instance.getStatus(uuid).should.eventually.equal('stubbed') | ||
}) | ||
@@ -225,3 +218,3 @@ }) | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
@@ -233,3 +226,3 @@ resolve({ access: accessToken, refresh: refreshToken }) | ||
.returns(new Promise(resolve => { | ||
resolve(uuid) | ||
resolve({ uuid }) | ||
})) | ||
@@ -242,9 +235,9 @@ sinon.stub(poller, 'do') | ||
await this.instance.analyze({ data }).should.eventually.equal(issues) | ||
await this.instance.analyze({ data }).should.eventually.deep.equal({ issues, uuid }) | ||
}) | ||
it('should reject with login failures', async () => { | ||
const errorMsg = 'Invalid MythX credentials for email address user@example.com given.' | ||
const errorMsg = 'Invalid MythX credentials for ethereum address 0x74B904af705Eb2D5a6CDc174c08147bED478a60d given.' | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise((resolve, reject) => { | ||
@@ -256,3 +249,3 @@ reject(new Error(errorMsg)) | ||
.returns(new Promise(resolve => { | ||
resolve(uuid) | ||
resolve({ uuid }) | ||
})) | ||
@@ -262,3 +255,3 @@ sinon.stub(poller, 'do') | ||
.returns(new Promise(resolve => { | ||
resolve(issues) | ||
resolve({ issues }) | ||
})) | ||
@@ -272,3 +265,3 @@ | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
@@ -285,3 +278,3 @@ resolve({ access: accessToken, refresh: refreshToken }) | ||
.returns(new Promise(resolve => { | ||
resolve(issues) | ||
resolve({ issues }) | ||
})) | ||
@@ -295,3 +288,3 @@ | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
@@ -303,3 +296,3 @@ resolve({ access: accessToken, refresh: refreshToken }) | ||
.returns(new Promise(resolve => { | ||
resolve(uuid) | ||
resolve({ uuid }) | ||
})) | ||
@@ -318,3 +311,3 @@ sinon.stub(poller, 'do') | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
@@ -326,12 +319,54 @@ resolve({ access: accessToken, refresh: refreshToken }) | ||
.returns(new Promise(resolve => { | ||
resolve(uuid) | ||
resolve({ uuid, status: 'Finished' }) | ||
})) | ||
sinon.stub(poller, 'getIssues') | ||
.withArgs(uuid, accessToken, parsedApiUrl) | ||
.returns(issues) | ||
sinon.stub(poller, 'do') | ||
.withArgs(uuid, accessToken, parsedApiUrl, undefined, timeout) | ||
.withArgs(uuid, accessToken, parsedApiUrl, timeout) | ||
.returns(new Promise(resolve => { | ||
resolve(issues) | ||
})) | ||
await this.instance.analyze({ data, timeout }).should.eventually.deep.equal({ issues, uuid }) | ||
poller.getIssues.restore() | ||
}) | ||
await this.instance.analyze({ data, timeout }).should.eventually.equal(issues) | ||
it('should pass default initial delay option to poller', async () => { | ||
const timeout = 40000 | ||
sinon.stub(login, 'do') | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
resolve({ access: accessToken, refresh: refreshToken }) | ||
})) | ||
sinon.stub(requester, 'do') | ||
.withArgs({ data, timeout }, accessToken, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
resolve({ uuid }) | ||
})) | ||
sinon.stub(poller, 'do') | ||
.withArgs(uuid, accessToken, parsedApiUrl, timeout, armlet.defaultInitialDelay, undefined) | ||
.resolves(issues) | ||
await this.instance.analyze({ data, timeout }).should.eventually.deep.equal({ issues, uuid }) | ||
}) | ||
it('should pass initial delay option to poller', async () => { | ||
const timeout = 40000 | ||
const initialDelay = 50000 | ||
sinon.stub(login, 'do') | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
resolve({ access: accessToken, refresh: refreshToken }) | ||
})) | ||
sinon.stub(requester, 'do') | ||
.withArgs({ data, timeout, initialDelay }, accessToken, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
resolve({ uuid }) | ||
})) | ||
sinon.stub(poller, 'do') | ||
.withArgs(uuid, accessToken, parsedApiUrl, timeout, initialDelay, undefined) | ||
.returns(new Promise(resolve => { | ||
resolve(issues) | ||
})) | ||
await this.instance.analyze({ data, timeout, initialDelay }).should.eventually.deep.equal({ issues, uuid }) | ||
}) | ||
}) | ||
@@ -346,3 +381,3 @@ | ||
.returns(new Promise(resolve => { | ||
resolve(uuid) | ||
resolve({ uuid }) | ||
})) | ||
@@ -355,3 +390,3 @@ sinon.stub(poller, 'do') | ||
await this.instance.analyze({ data }).should.eventually.equal(issues) | ||
await this.instance.analyze({ data }).should.eventually.deep.equal({ issues, uuid }) | ||
}) | ||
@@ -384,3 +419,3 @@ }) | ||
.returns(new Promise(resolve => { | ||
resolve(uuid) | ||
resolve({ uuid }) | ||
})) | ||
@@ -400,3 +435,3 @@ | ||
await this.instance.analyze({ data }).should.eventually.equal(issues) | ||
await this.instance.analyze({ data }).should.eventually.deep.equal({ issues, uuid }) | ||
}) | ||
@@ -418,3 +453,3 @@ | ||
.returns(new Promise(resolve => { | ||
resolve(uuid) | ||
resolve({ uuid }) | ||
})) | ||
@@ -428,3 +463,3 @@ | ||
await this.instance.analyze({ data }).should.eventually.equal(issues) | ||
await this.instance.analyze({ data }).should.eventually.deep.equal({ issues, uuid }) | ||
}) | ||
@@ -449,3 +484,3 @@ }) | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
@@ -466,3 +501,3 @@ resolve({ access: accessToken, refresh: refreshToken }) | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise((resolve, reject) => { | ||
@@ -483,3 +518,3 @@ reject(new Error(errorMsg)) | ||
sinon.stub(login, 'do') | ||
.withArgs(email, ethAddress, undefined, password, parsedApiUrl) | ||
.withArgs(ethAddress, password, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
@@ -551,36 +586,2 @@ resolve({ access: accessToken, refresh: refreshToken }) | ||
}) | ||
describe('as anonymous user', () => { | ||
beforeEach(() => { | ||
this.instance = new Client({ }, apiUrl) | ||
}) | ||
describe('analyze', () => { | ||
it('should login and chain requester and poller', async () => { | ||
sinon.stub(login, 'do') | ||
.withArgs(undefined, undefined, armlet.trialUserId, undefined, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
resolve({ access: accessToken, refresh: refreshToken }) | ||
})) | ||
sinon.stub(requester, 'do') | ||
.withArgs({ data }, accessToken, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
resolve(uuid) | ||
})) | ||
sinon.stub(poller, 'do') | ||
.withArgs(uuid, accessToken, parsedApiUrl) | ||
.returns(new Promise(resolve => { | ||
resolve(issues) | ||
})) | ||
await this.instance.analyze({ data }).should.eventually.equal(issues) | ||
}) | ||
afterEach(() => { | ||
requester.do.restore() | ||
poller.do.restore() | ||
login.do.restore() | ||
}) | ||
}) | ||
}) | ||
}) | ||
@@ -587,0 +588,0 @@ |
@@ -13,7 +13,5 @@ const nock = require('nock') | ||
const parsedApiUrl = new url.URL(apiUrl) | ||
const email = 'content' | ||
const ethAddress = '0x74B904af705Eb2D5a6CDc174c08147bED478a60d' | ||
const userId = '123456' | ||
const password = 'password' | ||
const auth = { email, ethAddress, userId, password } | ||
const auth = { ethAddress, password } | ||
const loginPath = '/v1/auth/login' | ||
@@ -29,9 +27,19 @@ const refresh = 'refresh-token' | ||
await login.do(email, ethAddress, userId, password, parsedApiUrl).should.eventually.deep.equal(jsonTokens) | ||
await login.do(ethAddress, password, parsedApiUrl).should.eventually.deep.equal(jsonTokens) | ||
}) | ||
it('should redirect refresh and access tokens', async () => { | ||
nock('http://localhost:3100') | ||
.post(loginPath, auth) | ||
.reply(200, jsonTokens) | ||
const parsedApiUrlHttp = new url.URL('http://localhost:3100') | ||
await login.do(ethAddress, password, parsedApiUrlHttp) | ||
.should.eventually.deep.equal(jsonTokens) | ||
}) | ||
it('should reject on api server connection failure', async () => { | ||
const invalidUrlObject = 'not-an-url-object' | ||
await login.do(email, ethAddress, userId, password, invalidUrlObject).should.be.rejectedWith(Error) | ||
await login.do(ethAddress, password, invalidUrlObject).should.be.rejectedWith(Error) | ||
}) | ||
@@ -44,3 +52,3 @@ | ||
await login.do(email, ethAddress, userId, password, parsedApiUrl).should.be.rejectedWith(Error, 'Invalid status code') | ||
await login.do(ethAddress, password, parsedApiUrl).should.be.rejectedWith('HTTP status 500') | ||
}) | ||
@@ -53,3 +61,3 @@ | ||
await login.do(email, ethAddress, userId, password, parsedApiUrl).should.be.rejectedWith(Error, 'JSON parse error') | ||
await login.do(ethAddress, password, parsedApiUrl).should.be.rejectedWith(Error, 'JSON parse error') | ||
}) | ||
@@ -62,3 +70,3 @@ | ||
await login.do(email, ethAddress, userId, password, parsedApiUrl).should.be.rejectedWith(Error, 'Refresh Token missing') | ||
await login.do(ethAddress, password, parsedApiUrl).should.be.rejectedWith(Error, 'Refresh Token missing') | ||
}) | ||
@@ -71,5 +79,5 @@ | ||
await login.do(email, ethAddress, userId, password, parsedApiUrl).should.be.rejectedWith(Error, 'Access Token missing') | ||
await login.do(ethAddress, password, parsedApiUrl).should.be.rejectedWith(Error, 'Access Token missing') | ||
}) | ||
}) | ||
}) |
@@ -32,3 +32,6 @@ const nock = require('nock') | ||
await requester.do(data, validApiKey, httpApiUrl).should.eventually.equal(uuid) | ||
await requester.do(data, validApiKey, httpApiUrl).should.eventually.deep.equal({ | ||
result: 'Queued', | ||
uuid | ||
}) | ||
}) | ||
@@ -48,3 +51,6 @@ | ||
await requester.do(data, validApiKey, httpsApiUrl).should.eventually.equal(uuid) | ||
await requester.do(data, validApiKey, httpsApiUrl).should.eventually.deep.equal({ | ||
result: 'Queued', | ||
uuid | ||
}) | ||
}) | ||
@@ -64,3 +70,6 @@ | ||
await requester.do(data, validApiKey, defaultApiUrl).should.eventually.equal(uuid) | ||
await requester.do(data, validApiKey, defaultApiUrl).should.eventually.deep.equal({ | ||
result: 'Queued', | ||
uuid | ||
}) | ||
}) | ||
@@ -154,3 +163,3 @@ | ||
await requester.do(data, validApiKey, defaultApiUrl).should.be.rejectedWith(SyntaxError) | ||
await requester.do(data, validApiKey, defaultApiUrl).should.be.rejectedWith('Non JSON data returned: non-json-response') | ||
}) | ||
@@ -157,0 +166,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
776419
12
41
18118
180
5
2
+ Addedisomorphic-fetch@^2.2.1
+ Addedomni-fetch@^0.2.3
+ Added@types/isomorphic-fetch@0.0.31(transitive)
+ Added@types/node@6.14.13(transitive)
+ Addedcaw@1.2.0(transitive)
+ Addeddeep-extend@0.6.0(transitive)
+ Addedencoding@0.1.13(transitive)
+ Addedget-proxy@1.1.0(transitive)
+ Addediconv-lite@0.6.3(transitive)
+ Addedini@1.3.8(transitive)
+ Addedis-obj@1.0.1(transitive)
+ Addedis-stream@1.1.0(transitive)
+ Addedisomorphic-fetch@2.2.1(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addednode-fetch@1.7.3(transitive)
+ Addedobject-assign@3.0.0(transitive)
+ Addedomni-fetch@0.2.3(transitive)
+ Addedrc@1.2.8(transitive)
+ Addedstrip-json-comments@2.0.1(transitive)
+ Addedtunnel-agent@0.4.3(transitive)
+ Addedwhatwg-fetch@3.6.20(transitive)
- Removedmoment@^2.24.0
- Removedmoment@2.30.1(transitive)