heroku-client
Advanced tools
Comparing version 2.4.3 to 3.0.0-alpha1
@@ -1,395 +0,317 @@ | ||
'use strict'; | ||
'use strict' | ||
var http = require('http'); | ||
var https = require('https'); | ||
var tunnel = require('tunnel-agent'); | ||
var extend = require('util')._extend; | ||
var q = require('q'); | ||
var URL = require('./url'); | ||
var pjson = require('../package.json'); | ||
var http = require('http') | ||
var https = require('https') | ||
var tunnel = require('tunnel-agent') | ||
var extend = require('util')._extend | ||
var URL = require('./url') | ||
var pjson = require('../package.json') | ||
var fs = require('fs') | ||
var path = require('path') | ||
module.exports = Request; | ||
/* | ||
* Create an object capable of making API | ||
* calls. Accepts custom request options and | ||
* a callback function. | ||
* Object capable of making API calls. | ||
*/ | ||
function Request(options, callback) { | ||
this.options = options || {}; | ||
this.debug = options.debug; | ||
this.debugHeaders = options.debugHeaders; | ||
if (this.debug) { | ||
q.longStackSupport = true; | ||
} | ||
var url = URL(options.host || 'https://api.heroku.com'); | ||
this.host = url.host; | ||
this.port = url.port; | ||
this.secure = url.secure; | ||
this.partial = options.partial; | ||
this.callback = callback; | ||
this.deferred = q.defer(); | ||
this.userAgent = options.userAgent || 'node-heroku-client/' + pjson.version; | ||
this.parseJSON = options.hasOwnProperty('parseJSON') ? options.parseJSON : true; | ||
this.nextRange = 'id ]..; max=1000'; | ||
this.logger = options.logger; | ||
this.cache = options.cache; | ||
this.middleware = options.middleware || function (_, cb) {cb();}; | ||
if (process.env.HEROKU_HTTP_PROXY_HOST) { | ||
var tunnelFunc; | ||
if (this.secure) { | ||
tunnelFunc = tunnel.httpsOverHttp; | ||
class Request { | ||
constructor (options) { | ||
this.options = options || {} | ||
this.debug = options.debug | ||
this.debugHeaders = options.debugHeaders | ||
var url = URL(options.host || 'https://api.heroku.com') | ||
this.host = url.host | ||
this.port = url.port | ||
this.secure = url.secure | ||
this.partial = options.partial | ||
this.userAgent = options.userAgent || 'node-heroku-client/' + pjson.version | ||
this.parseJSON = options.hasOwnProperty('parseJSON') ? options.parseJSON : true | ||
this.nextRange = 'id ..; max=1000' | ||
this.logger = options.logger | ||
this.middleware = options.middleware || function (_, cb) { cb() } | ||
this.certs = getCerts(this.debug) | ||
if (process.env.HEROKU_HTTP_PROXY_HOST) { | ||
var tunnelFunc | ||
if (this.secure) { | ||
tunnelFunc = tunnel.httpsOverHttp | ||
} else { | ||
tunnelFunc = tunnel.httpOverHttp | ||
} | ||
var agentOpts = { | ||
proxy: { | ||
host: process.env.HEROKU_HTTP_PROXY_HOST, | ||
port: process.env.HEROKU_HTTP_PROXY_PORT || 8080, | ||
proxyAuth: process.env.HEROKU_HTTP_PROXY_AUTH | ||
} | ||
} | ||
if (this.certs.length > 0) { | ||
agentOpts.ca = this.certs | ||
} | ||
this.agent = tunnelFunc(agentOpts) | ||
} else { | ||
tunnelFunc = tunnel.httpOverHttp; | ||
} | ||
this.agent = tunnelFunc({ | ||
proxy: { | ||
host: process.env.HEROKU_HTTP_PROXY_HOST, | ||
port: process.env.HEROKU_HTTP_PROXY_PORT || 8080, | ||
proxyAuth: process.env.HEROKU_HTTP_PROXY_AUTH | ||
if (this.secure) { | ||
this.agent = new https.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }) | ||
} else { | ||
this.agent = new http.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }) | ||
} | ||
}); | ||
} else { | ||
if (this.secure) { | ||
this.agent = new https.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }); | ||
} else { | ||
this.agent = new http.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }); | ||
} | ||
} | ||
} | ||
/* | ||
* Perform the actual API request. | ||
*/ | ||
request () { | ||
return new Promise((resolve, reject) => { | ||
this.resolve = resolve | ||
this.reject = reject | ||
var headers = extend({ | ||
'Accept': 'application/vnd.heroku+json; version=3', | ||
'Content-type': 'application/json', | ||
'User-Agent': this.userAgent, | ||
'Range': this.nextRange | ||
}, this.options.headers) | ||
// remove null|undefined headers | ||
for (var k in Object.keys(headers)) { | ||
if (headers[k] === null || headers[k] === undefined) { | ||
delete headers[k] | ||
} | ||
} | ||
/* | ||
* Instantiate a Request object and makes a | ||
* request, returning the request promise. | ||
*/ | ||
Request.request = function request(options, callback) { | ||
var req = new Request(options, function (err, body) { | ||
if (callback) { callback(err, body); } | ||
}); | ||
var requestOptions = { | ||
agent: this.agent, | ||
host: this.host, | ||
port: this.port, | ||
path: this.options.path, | ||
auth: this.options.auth || ':' + this.options.token, | ||
method: this.options.method || 'GET', | ||
rejectUnauthorized: this.options.rejectUnauthorized, | ||
headers: headers | ||
} | ||
if (this.certs.length > 0) { | ||
requestOptions.ca = this.certs | ||
} | ||
return req.request(); | ||
}; | ||
let req | ||
if (this.secure) { | ||
req = https.request(requestOptions, this.handleResponse.bind(this)) | ||
} else { | ||
req = http.request(requestOptions, this.handleResponse.bind(this)) | ||
} | ||
/* | ||
* Check for a cached response, then | ||
* perform an API request. Return the | ||
* request object's promise. | ||
*/ | ||
Request.prototype.request = function request() { | ||
this.getCache(this.performRequest.bind(this)); | ||
return this.deferred.promise; | ||
}; | ||
this.logRequest(req) | ||
this.writeBody(req) | ||
this.setRequestTimeout(req) | ||
/* | ||
* Perform the actual API request. | ||
*/ | ||
Request.prototype.performRequest = function performRequest() { | ||
var req; | ||
req.on('error', reject) | ||
this.options.headers = this.options.headers || {}; | ||
var headers = extend({ | ||
'Accept': 'application/vnd.heroku+json; version=3', | ||
'Content-type': 'application/json', | ||
'User-Agent': this.userAgent, | ||
'Range': this.nextRange | ||
}, this.options.headers); | ||
req.end() | ||
}) | ||
} | ||
// remove null|undefined headers | ||
for (var k in headers) { | ||
if (headers.hasOwnProperty(k)) { | ||
if (headers[k] === null || headers[k] === undefined) { | ||
delete headers[k]; | ||
/* | ||
* Handle an API response, returning the API response. | ||
*/ | ||
handleResponse (res) { | ||
this.middleware(res, () => { | ||
this.logResponse(res) | ||
if (res.statusCode === 304) { | ||
this.updateAggregate(this.cachedResponse.body) | ||
this.resolve(this.aggregate) | ||
return | ||
} | ||
} | ||
concat(res).then((data) => { | ||
if (this.debug) { | ||
console.error('<-- ' + data) | ||
} | ||
if (res.statusCode.toString().match(/^2\d{2}$/)) { | ||
this.handleSuccess(res, data) | ||
} else { | ||
this.handleFailure(res, data) | ||
} | ||
}) | ||
}) | ||
} | ||
var requestOptions = { | ||
agent: this.agent, | ||
host: this.host, | ||
port: this.port, | ||
path: this.options.path, | ||
auth: this.options.auth || ':' + this.options.token, | ||
method: this.options.method || 'GET', | ||
rejectUnauthorized: this.options.rejectUnauthorized, | ||
headers: headers | ||
}; | ||
if (this.cachedResponse) { | ||
headers['If-None-Match'] = this.cachedResponse.etag; | ||
logRequest (req) { | ||
if (this.debug) { | ||
console.error('--> ' + req.method + ' ' + req.path) | ||
} | ||
if (this.debugHeaders) { | ||
printHeaders(req._headers) | ||
} | ||
} | ||
if (this.secure) { | ||
req = https.request(requestOptions, this.handleResponse.bind(this)); | ||
} else { | ||
req = http.request(requestOptions, this.handleResponse.bind(this)); | ||
/* | ||
* Log the API response. | ||
*/ | ||
logResponse (res) { | ||
if (this.logger) { | ||
this.logger.log({ | ||
status: res.statusCode, | ||
content_length: res.headers['content-length'], | ||
request_id: res.headers['request-id'] | ||
}) | ||
} | ||
if (this.debug) { | ||
console.error('<-- ' + res.statusCode + ' ' + res.statusMessage) | ||
} | ||
if (this.debugHeaders) { | ||
printHeaders(res.headers) | ||
} | ||
} | ||
this.logRequest(req); | ||
this.writeBody(req); | ||
this.setRequestTimeout(req); | ||
req.on('error', this.handleError.bind(this)); | ||
req.end(); | ||
return this.deferred.promise; | ||
}; | ||
/* | ||
* Handle an API response, returning the API response. | ||
*/ | ||
Request.prototype.handleResponse = function handleResponse(res) { | ||
var self = this; | ||
this.middleware(res, function () { | ||
self.logResponse(res); | ||
if (res.statusCode === 304 && self.cachedResponse) { | ||
if (self.cachedResponse.nextRange) { | ||
self.nextRequest(self.cachedResponse.nextRange, self.cachedResponse.body); | ||
} else { | ||
self.updateAggregate(self.cachedResponse.body); | ||
self.deferred.resolve(self.aggregate); | ||
self.callback(null, self.aggregate); | ||
/* | ||
* If the request options include a body, | ||
* write the body to the request and set | ||
* an appropriate 'Content-length' header. | ||
*/ | ||
writeBody (req) { | ||
if (this.options.body) { | ||
var body = this.options.body | ||
if (this.options.json !== false) { body = JSON.stringify(body) } | ||
if (this.debug) { | ||
console.error('--> ' + body) | ||
} | ||
return; | ||
} | ||
concat(res, function (data) { | ||
if (self.debug) { | ||
console.error('<-- ' + data); | ||
} | ||
if (res.statusCode.toString().match(/^2\d{2}$/)) { | ||
self.handleSuccess(res, data); | ||
} else { | ||
self.handleFailure(res, data); | ||
} | ||
}); | ||
}); | ||
}; | ||
function printHeaders (headers) { | ||
var key; | ||
var value; | ||
for (key in headers) { | ||
if (headers.hasOwnProperty(key)) { | ||
value = key.toUpperCase() === 'AUTHORIZATION' ? 'REDACTED' : headers[key]; | ||
console.error(' ' + key + '=' + value); | ||
req.setHeader('Content-length', Buffer.byteLength(body, 'utf8')) | ||
req.write(body) | ||
} else { | ||
req.setHeader('Content-length', 0) | ||
} | ||
} | ||
} | ||
Request.prototype.logRequest = function logRequest(req) { | ||
if (this.debug) { | ||
console.error('--> ' + req.method + ' ' + req.path); | ||
} | ||
if (this.debugHeaders) { | ||
printHeaders(req._headers); | ||
} | ||
}; | ||
/* | ||
* If the request options include a timeout, | ||
* set the timeout and provide a callback | ||
* function in case the request exceeds the | ||
* timeout period. | ||
*/ | ||
setRequestTimeout (req) { | ||
if (!this.options.timeout) return | ||
/* | ||
* Log the API response. | ||
*/ | ||
Request.prototype.logResponse = function logResponse(res) { | ||
if (this.logger) { | ||
this.logger.log({ | ||
status : res.statusCode, | ||
content_length: res.headers['content-length'], | ||
request_id : res.headers['request-id'] | ||
}); | ||
req.setTimeout(this.options.timeout, () => { | ||
var err = new Error('Request took longer than ' + this.options.timeout + 'ms to complete.') | ||
req.abort() | ||
this.reject(err) | ||
}) | ||
} | ||
if (this.debug) { | ||
console.error('<-- ' + res.statusCode + ' ' + res.statusMessage); | ||
} | ||
if (this.debugHeaders) { | ||
printHeaders(res.headers); | ||
} | ||
}; | ||
/* | ||
* If the request options include a body, | ||
* write the body to the request and set | ||
* an appropriate 'Content-length' header. | ||
*/ | ||
Request.prototype.writeBody = function writeBody(req) { | ||
if (this.options.body) { | ||
var body = this.options.body; | ||
if (this.options.json !== false) { body = JSON.stringify(body); } | ||
if (this.debug) { | ||
console.error('--> ' + body); | ||
/* | ||
* Get the request body, and parse it (or not) as appropriate. | ||
* - Parse JSON by default. | ||
* - If parseJSON is `false`, it will not parse. | ||
*/ | ||
parseBody (body) { | ||
if (this.parseJSON) { | ||
return JSON.parse(body || '{}') | ||
} else { | ||
return body | ||
} | ||
req.setHeader('Content-length', Buffer.byteLength(body, 'utf8')); | ||
req.write(body); | ||
} else { | ||
req.setHeader('Content-length', 0); | ||
} | ||
}; | ||
/* | ||
* In the event of a non-successful API request, | ||
* fail with an appropriate error message and | ||
* status code. | ||
*/ | ||
handleFailure (res, buffer) { | ||
var message = 'Expected response to be successful, got ' + res.statusCode | ||
var err | ||
/* | ||
* If the request options include a timeout, | ||
* set the timeout and provide a callback | ||
* function in case the request exceeds the | ||
* timeout period. | ||
*/ | ||
Request.prototype.setRequestTimeout = function setRequestTimeout(req) { | ||
if (!this.options.timeout) { return; } | ||
err = new Error(message) | ||
err.statusCode = res.statusCode | ||
err.body = this.parseBody(buffer) | ||
req.setTimeout(this.options.timeout, function () { | ||
var err = new Error('Request took longer than ' + this.options.timeout + 'ms to complete.'); | ||
this.reject(err) | ||
} | ||
req.abort(); | ||
/* | ||
* In the event of a successful API response, | ||
* respond with the response body. | ||
*/ | ||
handleSuccess (res, buffer) { | ||
var body = this.parseBody(buffer) | ||
this.deferred.reject(err); | ||
this.callback(err); | ||
}.bind(this)); | ||
}; | ||
if (!this.partial && res.headers['next-range']) { | ||
this.nextRequest(res.headers['next-range'], body) | ||
} else { | ||
this.updateAggregate(body) | ||
this.resolve(this.aggregate) | ||
} | ||
} | ||
/* | ||
* Get the request body, and parse it (or not) as appropriate. | ||
* - Parse JSON by default. | ||
* - If parseJSON is `false`, it will not parse. | ||
*/ | ||
Request.prototype.parseBody = function parseBody(body) { | ||
if (this.parseJSON) { | ||
return JSON.parse(body || '{}'); | ||
} else { | ||
return body; | ||
/* | ||
* Since this request isn't the full response (206 or | ||
* 304 with a cached Next-Range), perform the next | ||
* request for more data. | ||
*/ | ||
nextRequest (nextRange, body) { | ||
this.updateAggregate(body) | ||
this.nextRange = nextRange | ||
// The initial range header passed in (if there was one), is no longer valid, and should no longer take precedence | ||
delete (this.options.headers.Range) | ||
this.request() | ||
} | ||
}; | ||
/* | ||
* In the event of an error in performing | ||
* the API request, reject the deferred | ||
* object and return an error to the callback. | ||
*/ | ||
Request.prototype.handleError = function handleError(err) { | ||
this.deferred.reject(err); | ||
this.callback(err); | ||
}; | ||
/* | ||
* If given an object, sets aggregate to object, | ||
* otherwise concats array onto aggregate. | ||
*/ | ||
updateAggregate (aggregate) { | ||
if (aggregate instanceof Array) { | ||
this.aggregate = this.aggregate || [] | ||
this.aggregate = this.aggregate.concat(aggregate) | ||
} else { | ||
this.aggregate = aggregate | ||
} | ||
} | ||
} | ||
function sslCertFile () { | ||
return process.env.SSL_CERT_FILE ? [process.env.SSL_CERT_FILE] : [] | ||
} | ||
/* | ||
* In the event of a non-successful API request, | ||
* fail with an appropriate error message and | ||
* status code. | ||
*/ | ||
Request.prototype.handleFailure = function handleFailure(res, buffer) { | ||
var callback = this.callback; | ||
var deferred = this.deferred; | ||
var message = 'Expected response to be successful, got ' + res.statusCode; | ||
var err; | ||
err = new Error(message); | ||
err.statusCode = res.statusCode; | ||
err.body = this.parseBody(buffer); | ||
deferred.reject(err); | ||
callback(err); | ||
}; | ||
/* | ||
* In the event of a successful API response, | ||
* respond with the response body. | ||
*/ | ||
Request.prototype.handleSuccess = function handleSuccess(res, buffer) { | ||
var callback = this.callback; | ||
var deferred = this.deferred; | ||
var body = this.parseBody(buffer); | ||
this.setCache(res, body); | ||
if (!this.partial && res.headers['next-range']) { | ||
this.nextRequest(res.headers['next-range'], body); | ||
function sslCertDir () { | ||
var certDir = process.env.SSL_CERT_DIR | ||
if (certDir) { | ||
return fs.readdirSync(certDir).map((f) => path.join(certDir, f)) | ||
} else { | ||
this.updateAggregate(body); | ||
deferred.resolve(this.aggregate); | ||
callback(null, this.aggregate); | ||
return [] | ||
} | ||
}; | ||
} | ||
function getCerts (debug) { | ||
var filenames = sslCertFile().concat(sslCertDir()) | ||
/* | ||
* Since this request isn't the full response (206 or | ||
* 304 with a cached Next-Range), perform the next | ||
* request for more data. | ||
*/ | ||
Request.prototype.nextRequest = function nextRequest(nextRange, body) { | ||
this.updateAggregate(body); | ||
this.nextRange = nextRange; | ||
// The initial range header passed in (if there was one), is no longer valid, and should no longer take precedence | ||
delete (this.options.headers.Range); | ||
this.request(); | ||
}; | ||
/* | ||
* If given an object, sets aggregate to object, | ||
* otherwise concats array onto aggregate. | ||
*/ | ||
Request.prototype.updateAggregate = function updateAggregate(aggregate) { | ||
if (aggregate instanceof Array) { | ||
this.aggregate = this.aggregate || []; | ||
this.aggregate = this.aggregate.concat(aggregate); | ||
} else { | ||
this.aggregate = aggregate; | ||
if (filenames.length > 0 && debug) { | ||
console.error('Adding the following trusted certificate authorities') | ||
} | ||
}; | ||
/* | ||
* If the cache client is alive, get the | ||
* cached response from the cache. | ||
*/ | ||
Request.prototype.getCache = function getCache(callback) { | ||
if (!this.cache) { return callback(null); } | ||
var key = this.getCacheKey(); | ||
var self = this; | ||
this.cache.store.get(key, function (err, res) { | ||
if (err) { return self.deferred.reject(err); } | ||
self.cachedResponse = res ? self.cache.encryptor.decrypt(res.toString()) : res; | ||
callback(); | ||
}); | ||
}; | ||
return filenames.map(function (filename) { | ||
if (debug) { | ||
console.error(' ' + filename) | ||
} | ||
return fs.readFileSync(filename) | ||
}) | ||
} | ||
/* | ||
* If the cache client is alive, write the | ||
* provided response and body to the cache. | ||
*/ | ||
Request.prototype.setCache = function setCache(res, body) { | ||
if ((!this.cache) || !(res.headers.etag)) { return; } | ||
var key = this.getCacheKey(); | ||
var value = { | ||
body : body, | ||
etag : res.headers.etag, | ||
nextRange: res.headers['next-range'] | ||
}; | ||
if (this.debug) { | ||
console.error('<-- writing to cache'); | ||
function printHeaders (headers) { | ||
var key | ||
var value | ||
for (key in headers) { | ||
if (headers.hasOwnProperty(key)) { | ||
value = key.toUpperCase() === 'AUTHORIZATION' ? 'REDACTED' : headers[key] | ||
console.error(' ' + key + '=' + value) | ||
} | ||
} | ||
} | ||
value = this.cache.encryptor.encrypt(value); | ||
this.cache.store.set(key, value); | ||
}; | ||
function concat (stream) { | ||
return new Promise((resolve) => { | ||
var strings = [] | ||
stream.on('data', (data) => strings.push(data)) | ||
stream.on('end', () => resolve(strings.join(''))) | ||
}) | ||
} | ||
/* | ||
* Returns a cache key comprising the request path, | ||
* the 'Next Range' header, and the user's API token. | ||
*/ | ||
Request.prototype.getCacheKey = function getCacheKey() { | ||
var path = JSON.stringify([this.options.path, this.nextRange, this.options.token]); | ||
return this.cache.encryptor.hmac(path); | ||
}; | ||
function concat (stream, callback) { | ||
var strings = []; | ||
stream.on('data', function (data) { | ||
strings.push(data); | ||
}); | ||
stream.on('end', function () { | ||
callback(strings.join('')); | ||
}); | ||
} | ||
module.exports = Request |
@@ -1,22 +0,22 @@ | ||
'use strict'; | ||
'use strict' | ||
var url = require('url'); | ||
var url = require('url') | ||
module.exports = function(u) { | ||
module.exports = function (u) { | ||
if (u.indexOf('http') !== 0 && u.indexOf('https') !== 0) { | ||
u = 'https://' + u; | ||
u = 'https://' + u | ||
} | ||
var uu = url.parse(u); | ||
var port = uu.port; | ||
var uu = url.parse(u) | ||
var port = uu.port | ||
if (!port) { | ||
if (uu.protocol === 'https:') { | ||
port = '443'; | ||
port = '443' | ||
} else { | ||
port = '80'; | ||
port = '80' | ||
} | ||
} | ||
var secure = uu.protocol === 'https:' || uu.port === '443'; | ||
var secure = uu.protocol === 'https:' || uu.port === '443' | ||
return { host: uu.hostname, port: parseInt(port), secure: secure }; | ||
}; | ||
return { host: uu.hostname, port: parseInt(port), secure: secure } | ||
} |
{ | ||
"name": "heroku-client", | ||
"description": "A wrapper for the Heroku v3 API", | ||
"version": "2.4.3", | ||
"author": "Jonathan Clem", | ||
"version": "3.0.0-alpha1", | ||
"author": "Jeff Dickey", | ||
"bugs": { | ||
@@ -11,2 +11,3 @@ "url": "https://github.com/heroku/node-heroku-client/issues" | ||
"Jonathan Clem", | ||
"Jeff Dickey", | ||
"Ray McDermott", | ||
@@ -23,24 +24,10 @@ "Bob Zoller", | ||
"devDependencies": { | ||
"jasmine-node": "^1.14.5", | ||
"jshint": "^2.5.3", | ||
"simple-encryptor": "^1.0.3" | ||
"ava": "0.14.0", | ||
"codecov": "1.0.1", | ||
"nock": "8.0.0", | ||
"nyc": "6.4.4", | ||
"standard": "7.1.0" | ||
}, | ||
"jshintConfig": { | ||
"curly": true, | ||
"eqeqeq": true, | ||
"forin": true, | ||
"globals": { | ||
"afterEach": false, | ||
"before": false, | ||
"beforeEach": false, | ||
"describe": false, | ||
"expect": false, | ||
"it": false, | ||
"jasmine": false, | ||
"spyOn": false | ||
}, | ||
"node": true, | ||
"quotmark": "single", | ||
"undef": true, | ||
"unused": true | ||
"engines": { | ||
"node": ">=4.0.0" | ||
}, | ||
@@ -51,3 +38,2 @@ "keywords": [ | ||
"license": "MIT", | ||
"main": "./lib/heroku.js", | ||
"repository": { | ||
@@ -58,4 +44,4 @@ "type": "git", | ||
"scripts": { | ||
"test": "jasmine-node spec && jshint lib spec" | ||
"test": "nyc ava && standard" | ||
} | ||
} |
@@ -1,3 +0,7 @@ | ||
# heroku-client [![Build Status](https://travis-ci.org/heroku/node-heroku-client.png?branch=master)](https://travis-ci.org/heroku/node-heroku-client) | ||
# heroku-client | ||
[![Build Status](https://travis-ci.org/heroku/node-heroku-client.png?branch=master)](https://travis-ci.org/heroku/node-heroku-client) | ||
[![codecov](https://codecov.io/gh/heroku/node-heroku-client/branch/master/graph/badge.svg)](https://codecov.io/gh/heroku/node-heroku-client) | ||
[![Code Climate](https://codeclimate.com/github/heroku/node-heroku-client/badges/gpa.svg)](https://codeclimate.com/github/heroku/node-heroku-client) | ||
A wrapper around the [v3 Heroku API][platform-api-reference]. | ||
@@ -4,0 +8,0 @@ |
Sorry, the diff of this file is not supported yet
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
294
3
19960
5
84107
8
332
2
10