heroku-cli-util
Advanced tools
Comparing version 5.1.0 to 5.2.0
@@ -15,30 +15,2 @@ 'use strict'; | ||
Heroku.configure = function configure(config) { | ||
var cache = config.cache; | ||
var key = config.key || process.env.HEROKU_CLIENT_ENCRYPTION_SECRET; | ||
if (cache === true) { | ||
cache = require('memjs').Client.create(); | ||
} | ||
if (cache) { | ||
if (typeof(cache.get) !== 'function' || typeof(cache.set) !== 'function') { | ||
console.error('cache must define get(key, cb(err, value)) and set(key, value) functions'); | ||
process.exit(1); | ||
} | ||
if (!key) { | ||
console.error('Must supply key or set HEROKU_CLIENT_ENCRYPTION_SECRET environment variable in order to cache'); | ||
process.exit(1); | ||
} | ||
Request.connectCacheClient({ | ||
cache: cache, | ||
key : key | ||
}); | ||
} | ||
return this; | ||
}; | ||
Heroku.request = Request.request; | ||
@@ -45,0 +17,0 @@ |
'use strict'; | ||
var http = require('http'); | ||
var https = require('https'); | ||
var concat = require('concat-stream'); | ||
var lazy = require('lazy.js'); | ||
var logfmt = require('logfmt'); | ||
var tunnel = require('tunnel-agent'); | ||
var extend = require('util')._extend; | ||
var q = require('q'); | ||
var pjson = require('../package.json'); | ||
var cache; | ||
var encryptor; | ||
module.exports = Request; | ||
@@ -29,3 +24,2 @@ | ||
this.host = options.host || 'api.heroku.com'; | ||
this.log = options.log; | ||
this.partial = options.partial; | ||
@@ -37,7 +31,14 @@ this.callback = callback; | ||
this.nextRange = 'id ]..; max=1000'; | ||
this.logger = logfmt.namespace({ | ||
source: 'heroku-client', | ||
method: options.method || 'GET', | ||
path : options.path | ||
}).time(); | ||
this.logger = options.logger; | ||
this.cache = options.cache; | ||
if (process.env.HEROKU_HTTP_PROXY_HOST) { | ||
this.agent = tunnel.httpsOverHttp({ | ||
proxy: { | ||
host: process.env.HEROKU_HTTP_PROXY_HOST, | ||
port: process.env.HEROKU_HTTP_PROXY_PORT || 8080 | ||
} | ||
}); | ||
} else { | ||
this.agent = new https.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }); | ||
} | ||
} | ||
@@ -58,3 +59,2 @@ | ||
/* | ||
@@ -70,16 +70,10 @@ * Check for a cached response, then | ||
/* | ||
* Perform the actual API request. | ||
*/ | ||
Request.prototype.performRequest = function performRequest(cachedResponse) { | ||
var defaultRequestOptions, | ||
headers, | ||
key, | ||
requestOptions, | ||
req; | ||
Request.prototype.performRequest = function performRequest() { | ||
var req; | ||
this.cachedResponse = cachedResponse; | ||
headers = { | ||
this.options.headers = this.options.headers || {}; | ||
var headers = extend({ | ||
'Accept': 'application/vnd.heroku+json; version=3', | ||
@@ -89,17 +83,9 @@ 'Content-type': 'application/json', | ||
'Range': this.nextRange | ||
}; | ||
}, this.options.headers); | ||
this.options.headers = this.options.headers || {}; | ||
for (key in this.options.headers) { | ||
if (this.options.headers.hasOwnProperty(key)) { | ||
headers[key] = this.options.headers[key]; | ||
} | ||
} | ||
if (this.cachedResponse) { | ||
headers['If-None-Match'] = this.cachedResponse.etag; | ||
} | ||
defaultRequestOptions = { | ||
var requestOptions = { | ||
agent: this.agent, | ||
host: this.host, | ||
port: 443, | ||
path: this.options.path, | ||
auth: this.options.auth || ':' + this.options.token, | ||
@@ -111,11 +97,8 @@ method: this.options.method || 'GET', | ||
requestOptions = this.getRequestOptions(defaultRequestOptions); | ||
if (process.env.HEROKU_HTTP_PROXY_HOST) { | ||
headers.Host = this.host; | ||
req = http.request(requestOptions, this.handleResponse.bind(this)); | ||
} else { | ||
req = https.request(requestOptions, this.handleResponse.bind(this)); | ||
if (this.cachedResponse) { | ||
headers['If-None-Match'] = this.cachedResponse.etag; | ||
} | ||
req = https.request(requestOptions, this.handleResponse.bind(this)); | ||
if (this.debug) { | ||
@@ -131,41 +114,12 @@ console.error('--> ' + req.method + ' ' + req.path); | ||
req.end(); | ||
}; | ||
/* | ||
* Set return the correct request options, based on whether or not we're using | ||
* an HTTP proxy. | ||
*/ | ||
Request.prototype.getRequestOptions = function getRequestOptions(defaultOptions) { | ||
var requestOptions; | ||
if (process.env.HEROKU_HTTP_PROXY_HOST) { | ||
requestOptions = { | ||
agent: new http.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }), | ||
host : process.env.HEROKU_HTTP_PROXY_HOST, | ||
port : process.env.HEROKU_HTTP_PROXY_PORT || 8080, | ||
path : 'https://' + this.host + this.options.path | ||
}; | ||
} else { | ||
requestOptions = { | ||
agent: new https.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }), | ||
host : this.host, | ||
port : 443, | ||
path : this.options.path | ||
}; | ||
} | ||
return lazy(requestOptions).merge(defaultOptions).toObject(); | ||
return this.deferred.promise; | ||
}; | ||
/* | ||
* Handle an API response, returning the | ||
* cached body if it's still valid, or the | ||
* new API response. | ||
* Handle an API response, returning the API response. | ||
*/ | ||
Request.prototype.handleResponse = function handleResponse(res) { | ||
var self = this; | ||
var resReader = concat(directResponse); | ||
var self = this; | ||
this.logResponse(res); | ||
if (res.statusCode === 304 && this.cachedResponse) { | ||
@@ -179,7 +133,5 @@ if (this.cachedResponse.nextRange) { | ||
} | ||
} else { | ||
res.pipe(resReader); | ||
return; | ||
} | ||
function directResponse(data) { | ||
concat(res, function (data) { | ||
if (self.debug) { | ||
@@ -193,3 +145,3 @@ console.error('<-- ' + data); | ||
} | ||
} | ||
}); | ||
}; | ||
@@ -202,3 +154,3 @@ | ||
Request.prototype.logResponse = function logResponse(res) { | ||
if (this.log) { | ||
if (this.logger) { | ||
this.logger.log({ | ||
@@ -301,4 +253,3 @@ status : res.statusCode, | ||
* In the event of a successful API response, | ||
* write the response to the cache and resolve | ||
* with the response body. | ||
* respond with the response body. | ||
*/ | ||
@@ -335,2 +286,14 @@ Request.prototype.handleSuccess = function handleSuccess(res, buffer) { | ||
/* | ||
* 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; | ||
} | ||
}; | ||
@@ -342,13 +305,12 @@ /* | ||
Request.prototype.getCache = function getCache(callback) { | ||
if (!cache) { return callback(null); } | ||
if (!this.cache) { return callback(null); } | ||
var key = this.getCacheKey(); | ||
cache.get(key, function (err, res) { | ||
res = res ? encryptor.decrypt(res) : res; | ||
callback(res); | ||
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(); | ||
}); | ||
}; | ||
/* | ||
@@ -359,3 +321,3 @@ * If the cache client is alive, write the | ||
Request.prototype.setCache = function setCache(res, body) { | ||
if ((!cache) || !(res.headers.etag)) { return; } | ||
if ((!this.cache) || !(res.headers.etag)) { return; } | ||
@@ -369,7 +331,10 @@ var key = this.getCacheKey(); | ||
value = encryptor.encrypt(value); | ||
cache.set(key, value); | ||
if (this.debug) { | ||
console.error('<-- writing to cache'); | ||
} | ||
value = this.cache.encryptor.encrypt(value); | ||
this.cache.store.set(key, value); | ||
}; | ||
/* | ||
@@ -381,26 +346,13 @@ * Returns a cache key comprising the request path, | ||
var path = JSON.stringify([this.options.path, this.nextRange, this.options.token]); | ||
return encryptor.hmac(path); | ||
return this.cache.encryptor.hmac(path); | ||
}; | ||
/* | ||
* 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; | ||
} | ||
}; | ||
/* | ||
* Connect a cache client. | ||
*/ | ||
Request.connectCacheClient = function connectCacheClient(opts) { | ||
cache = opts.cache; | ||
encryptor = require('simple-encryptor')(opts.key); | ||
}; | ||
function concat (stream, callback) { | ||
var strings = []; | ||
stream.on('data', function (data) { | ||
strings.push(data); | ||
}); | ||
stream.on('end', function () { | ||
callback(strings.join('')); | ||
}); | ||
} |
@@ -125,6 +125,7 @@ { | ||
"shasum": "1cf160cd420956d674fd15af6d94291271d38b78", | ||
"tarball": "http://registry.npmjs.org/inflection/-/inflection-1.7.1.tgz" | ||
"tarball": "http://heroku-npm.herokuapp.com/inflection/-/inflection-1.7.1.tgz" | ||
}, | ||
"directories": {}, | ||
"_resolved": "https://registry.npmjs.org/inflection/-/inflection-1.7.1.tgz" | ||
"_resolved": "https://heroku-npm.herokuapp.com/inflection/-/inflection-1.7.1.tgz", | ||
"readme": "ERROR: No README data found!" | ||
} |
@@ -77,3 +77,3 @@ { | ||
"type": "git", | ||
"url": "https://github.com/dreamerslab/node.inflection.git" | ||
"url": "git+https://github.com/dreamerslab/node.inflection.git" | ||
}, | ||
@@ -113,6 +113,7 @@ "engines": [ | ||
"shasum": "cbd160da9f75b14c3cc63578d4f396784bf3014e", | ||
"tarball": "http://registry.npmjs.org/inflection/-/inflection-1.3.8.tgz" | ||
"tarball": "http://heroku-npm.herokuapp.com/inflection/-/inflection-1.3.8.tgz" | ||
}, | ||
"directories": {}, | ||
"_resolved": "https://registry.npmjs.org/inflection/-/inflection-1.3.8.tgz" | ||
"_resolved": "https://heroku-npm.herokuapp.com/inflection/-/inflection-1.3.8.tgz", | ||
"readme": "ERROR: No README data found!" | ||
} |
@@ -11,3 +11,3 @@ { | ||
"type": "git", | ||
"url": "https://github.com/jclem/path-proxy" | ||
"url": "git+https://github.com/jclem/path-proxy.git" | ||
}, | ||
@@ -37,3 +37,3 @@ "keywords": [ | ||
"shasum": "18e8a36859fc9d2f1a53b48dee138543c020de5e", | ||
"tarball": "http://registry.npmjs.org/path-proxy/-/path-proxy-1.0.0.tgz" | ||
"tarball": "http://heroku-npm.herokuapp.com/path-proxy/-/path-proxy-1.0.0.tgz" | ||
}, | ||
@@ -54,3 +54,3 @@ "_from": "path-proxy@>=1.0.0 <2.0.0", | ||
"_shasum": "18e8a36859fc9d2f1a53b48dee138543c020de5e", | ||
"_resolved": "https://registry.npmjs.org/path-proxy/-/path-proxy-1.0.0.tgz" | ||
"_resolved": "https://heroku-npm.herokuapp.com/path-proxy/-/path-proxy-1.0.0.tgz" | ||
} |
@@ -116,6 +116,6 @@ { | ||
"shasum": "55705bcd93c5f3673530c2c2cbc0c2b3addc286e", | ||
"tarball": "http://registry.npmjs.org/q/-/q-1.4.1.tgz" | ||
"tarball": "http://localhost:3000/q/-/q-1.4.1.tgz" | ||
}, | ||
"_resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", | ||
"_resolved": "http://localhost:3000/q/-/q-1.4.1.tgz", | ||
"readme": "ERROR: No README data found!" | ||
} |
{ | ||
"name": "heroku-client", | ||
"description": "A wrapper for the Heroku v3 API", | ||
"version": "1.11.0", | ||
"version": "2.0.0", | ||
"author": { | ||
@@ -26,14 +26,11 @@ "name": "Jonathan Clem" | ||
"dependencies": { | ||
"concat-stream": "^1.4.8", | ||
"inflection": "^1.7.0", | ||
"lazy.js": "^0.4.0", | ||
"logfmt": "^1.1.2", | ||
"memjs": "^0.8.5", | ||
"path-proxy": "^1.0", | ||
"q": "^1.2.0", | ||
"simple-encryptor": "^1.0.2" | ||
"tunnel-agent": "^0.4.0" | ||
}, | ||
"devDependencies": { | ||
"jasmine-node": "^1.14.5", | ||
"jshint": "^2.5.3" | ||
"jshint": "^2.5.3", | ||
"simple-encryptor": "^1.0.3" | ||
}, | ||
@@ -72,33 +69,9 @@ "jshintConfig": { | ||
}, | ||
"gitHead": "f831875dd9e42247e1d0c608380a1a539679089a", | ||
"readme": "# heroku-client [![Build Status](https://travis-ci.org/heroku/node-heroku-client.png?branch=master)](https://travis-ci.org/heroku/node-heroku-client)\n\nA wrapper around the [v3 Heroku API][platform-api-reference].\n\n- [Install](#install)\n- [Documentation](#documentation)\n- [Usage](#usage)\n - [Generic Requests](#generic-requests)\n - [Promises](#promises)\n - [Generators](#generators)\n - [HTTP Proxies](#http-proxies)\n- [Caching](#caching)\n - [Custom caching](#custom-caching)\n- [Contributing](#contributing)\n - [Updating resources](#updating-resources)\n - [Generating documentation](#generating-documentation)\n - [Running tests](#running-tests)\n\n## Install\n\n```sh\n$ npm install heroku-client --save\n```\n\n## Documentation\n\nDocs are auto-generated and live in the\n[docs directory](https://github.com/heroku/node-heroku-client/tree/master/docs).\n\n## Usage\n\n`heroku-client` works by providing functions that return proxy objects for\ninteracting with different resources through the Heroku API.\n\nTo begin, require the Heroku module and create a client, passing in an API\ntoken:\n\n```javascript\nvar Heroku = require('heroku-client'),\n heroku = new Heroku({ token: process.env.HEROKU_API_TOKEN });\n```\n\nThe simplest example is listing a user's apps. First, we call `heroku.apps()`,\nwhich returns a proxy object to the /apps endpoint, then we call `list()` to\nactually perform the API call:\n\n```javascript\nheroku.apps().list(function (err, apps) {\n // `apps` is a parsed JSON response from the API\n});\n```\n\nThe advantage of using proxy objects is that they are reusable. Let's get the\ninfo for the user's app \"my-app\", get the dynos for the app, and\nremove a collaborator:\n\n```javascript\nvar app = heroku.apps('my-app');\n\napp.info(function (err, app) {\n // Details about the `app`\n});\n\napp.dynos().list(function (err, dynos) {\n // List of the app's `dynos`\n});\n\napp.collaborators('user@example.com').delete(function (err, collaborator) {\n // The `collaborator` has been removed unless `err`\n});\n```\n\nRequests that require a body are easy, as well. Let's add a collaborator to\nthe user's app \"another-app\":\n\n```javascript\nvar app = heroku.apps('another-app'),\n user = { email: 'new-user@example.com' };\n\napp.collaborators().create({ user: user }, function (err, collaborator) {\n // `collaborator` is the newly added collaborator unless `err`\n});\n```\n\n### Generic Requests\n\nheroku-client has `get`, `post`, `patch`, and `delete` functions which can make\nrequests with the specified HTTP method to any endpoint:\n\n```javascript\nheroku.get('/apps', function (err, apps) {\n});\n\n// Request body is optional on both `post` and `patch`\nheroku.post('/apps', function (err, app) {\n});\n\nheroku.post('/apps', { name: 'my-new-app' }, function (err, app) {\n});\n\nheroku.patch('/apps/my-app', { name: 'my-renamed-app' }, function (err, app) {\n});\n\nheroku.delete('/apps/my-old-app', function (err, app) {\n});\n```\n\nThere is also an even more generic `request` function that can accept many more\noptions:\n\n```javascript\nheroku.request({\n method: 'GET',\n path: '/apps',\n headers: {\n 'Foo': 'Bar'\n },\n parseJSON: false\n}, function (err, responseBody) {\n});\n```\n\n### Promises\n\nheroku-client works with Node-style callbacks, but also implements promises with\nthe [Q][q] library.\n\n```javascript\nvar q = require('q');\n\n// Fetches dynos for all of my apps.\nheroku.apps().list().then(function (apps) {\n\n return q.all(apps.map(function (app) {\n return heroku.apps(app.name).dynos().list();\n }));\n\n}).then(function (dynos) {\n\n console.log(dynos);\n\n});\n```\n\n### Generators\n\nIt's easy to get heroku-client working with [generators][generators]. In this\nexample, I'll use the [co][co] library to wrap a function that will get the list\nof all of my apps, and then get the dynos for each of those apps:\n\n```javascript\nlet co = require('co');\nlet heroku = require('heroku-client');\nlet hk = heroku.createClient({ token: process.env.HEROKU_API_KEY });\n\nlet main = function* () {\n let apps = yield hk.apps().list();\n let dynos = yield apps.map(getDynos);\n\n console.log(dynos);\n\n function getDynos(app) {\n return hk.apps(app.name).dynos().list();\n }\n};\n\nco(main)();\n```\n\nAs long as you're using Node >= 0.11, you can run this script with:\n\n```sh\n$ node --harmony --use-strict file.js\n```\n\nHooray, no callbacks or promises in sight!\n\n### HTTP Proxies\n\nIf you'd like to make requests through an HTTP proxy, set the\n`HEROKU_HTTP_PROXY_HOST` environment variable with your proxy host, and\n`HEROKU_HTTP_PROXY_PORT` with the desired port (defaults to 8080). heroku-client\nwill then make requests through this proxy instead of directly to\napi.heroku.com.\n\n## Caching\n\nheroku-client can optionally perform caching of API requests.\n\nheroku-client will cache any response from the Heroku API that comes with an\n`ETag` header, and each response is cached individually (i.e. even though the\nclient might make multiple calls for a user's apps and then aggregate them into\na single JSON array, each required API call is individually cached). For each\nAPI request it performs, heroku-client sends an `If-None-Match` header if there\nis a cached response for the API request. If API returns a 304 response code,\nheroku-client returns the cached response. Otherwise, it writes the new API\nresponse to the cache and returns that.\n\nTo tell heroku-client to perform caching, add a config object to the options\nwith store and encryptor objects. These can be instances of memjs and\nsimple-encryptor, respectively.\n\n```js\nvar Heroku = require('heroku-client');\nvar memjs = require('memjs').Client.create();\nvar encryptor = require('simple-encryptor')(SECRET_CACHE_KEY);\nvar hk = new Heroku({\n cache: { store: memjs, encryptor: encryptor }\n});\n```\n\n### Custom caching\n\nAlternatively you can specify a custom cache implementation. Your custom implementation must define `get(key, cb(err, value))` and `set(key, value)` functions.\n\nHere's a sample implementation that uses Redis to cache API responses for 5-minutes each:\n\n```javascript\nvar redis = require('redis');\nvar client = redis.createClient();\nvar cacheTtlSecs = 5 * 60; // 5 minutes\n\nvar redisStore = {\n get: function(key, cb) {\n // Namespace the keys:\n var redisKey = 'heroku:api:' + key;\n client.GET(redisKey, cb);\n },\n\n set: function(key, value) {\n // Namespace the keys:\n var redisKey = 'heroku:api:' + key;\n client.SETEX(redisKey, cacheTtlSecs, value, function(err) {\n // ignore errors on set\n });\n }\n};\n\nvar encryptor = require('simple-encryptor')(SECRET_CACHE_KEY);\nvar Heroku = require('heroku-client');\nvar hk = new Heroku({\n cache: {store: redisStore, encryptor: encryptor}\n});\n```\n\n## Contributing\n\n### Updating resources\n\nTo fetch the latest schema, generate documentation, and run the tests:\n\n```sh\n$ bin/update\n```\n\nInspect your changes, and\n[bump the version number accordingly](http://semver.org/) when cutting a\nrelease.\n\n### Generating documentation\n\nDocumentation for heroku-client is auto-generated from\n[the API schema](https://github.com/heroku/node-heroku-client/blob/master/lib/schema.js).\n\nDocs are generated like so:\n\n```bash\n$ bin/docs\n```\n\nGenerating docs also runs a cursory test, ensuring that every documented\nfunction *is* a function that can be called.\n\n### Running tests\n\nheroku-client uses [jasmine-node][jasmine-node] for tests:\n\n```bash\n$ npm test\n```\n\n[platform-api-reference]: https://devcenter.heroku.com/articles/platform-api-reference\n[q]: https://github.com/kriskowal/q\n[memjs]: https://github.com/alevy/memjs\n[bin_secret]: https://github.com/heroku/node-heroku-client/blob/master/bin/secret\n[memcachier]: https://www.memcachier.com\n[jasmine-node]: https://github.com/mhevery/jasmine-node\n[generators]: https://github.com/JustinDrake/node-es6-examples#generators\n[co]: https://github.com/visionmedia/co\n", | ||
"readmeFilename": "README.md", | ||
"gitHead": "23aaf449ab35c4fdce6ec4bbe74aed5a58d9ebd2", | ||
"homepage": "https://github.com/heroku/node-heroku-client#readme", | ||
"_id": "heroku-client@1.11.0", | ||
"_shasum": "9ff0e041a3d1b363a72fa01ff876b316f6985773", | ||
"_from": "heroku-client@>=1.11.0 <2.0.0", | ||
"_npmVersion": "2.10.0", | ||
"_nodeVersion": "2.0.1", | ||
"_npmUser": { | ||
"name": "dickeyxxx", | ||
"email": "jeff@dickeyxxx.com" | ||
}, | ||
"maintainers": [ | ||
{ | ||
"name": "jackca", | ||
"email": "hi@janderson.me" | ||
}, | ||
{ | ||
"name": "dickeyxxx", | ||
"email": "jeff@dickeyxxx.com" | ||
}, | ||
{ | ||
"name": "heroku", | ||
"email": "npmjs@heroku.com" | ||
} | ||
], | ||
"dist": { | ||
"shasum": "9ff0e041a3d1b363a72fa01ff876b316f6985773", | ||
"tarball": "http://registry.npmjs.org/heroku-client/-/heroku-client-1.11.0.tgz" | ||
}, | ||
"directories": {}, | ||
"_resolved": "https://registry.npmjs.org/heroku-client/-/heroku-client-1.11.0.tgz" | ||
"_id": "heroku-client@2.0.0", | ||
"_shasum": "610b5da330108679e9e244f43b947bad8ef6200c", | ||
"_from": "heroku-client@>=2.0.0 <3.0.0" | ||
} |
@@ -13,3 +13,2 @@ # heroku-client [![Build Status](https://travis-ci.org/heroku/node-heroku-client.png?branch=master)](https://travis-ci.org/heroku/node-heroku-client) | ||
- [Caching](#caching) | ||
- [Caching with memjs](#caching-with-memjs) | ||
- [Custom caching](#custom-caching) | ||
@@ -201,33 +200,15 @@ - [Contributing](#contributing) | ||
To tell heroku-client to perform caching, call the `configure` function. | ||
To tell heroku-client to perform caching, add a config object to the options | ||
with store and encryptor objects. These can be instances of memjs and | ||
simple-encryptor, respectively. | ||
Caching requires an encryption key to encrypt the results prior to caching. | ||
This must be set in the environment variable HEROKU_CLIENT_ENCRYPTION_SECRET. | ||
`HEROKU_CLIENT_ENCRYPTION_SECRET` should be a long, random string of characters. | ||
heroku-client includes [`bin/secret`][bin_secret] as one way of generating | ||
values for this variable. **Do not publish this secret or commit it to source | ||
control. If it's compromised, flush your memcache and generate a new encryption | ||
secret.** | ||
### Caching with memjs | ||
If `cache` is the boolean value `true` then heroku-client will use `memjs` for caching. | ||
Example: | ||
```javascript | ||
var Heroku = require('heroku').configure({ cache: true }); | ||
```js | ||
var Heroku = require('heroku-client'); | ||
var memjs = require('memjs').Client.create(); | ||
var encryptor = require('simple-encryptor')(SECRET_CACHE_KEY); | ||
var hk = new Heroku({ | ||
cache: { store: memjs, encryptor: encryptor } | ||
}); | ||
``` | ||
This requires a `MEMCACHIER_SERVERS` environment variable, as well as a | ||
`HEROKU_CLIENT_ENCRYPTION_SECRET` environment variable that heroku-client uses | ||
to build cache keys and encrypt cache contents. | ||
`MEMCACHIER_SERVERS` can be a single `hostname:port` memache address, or a | ||
comma-separated list of memcache addresses, e.g. | ||
`example.com:11211,example.net:11211`. Note that while the environment variable | ||
that memjs looks for is | ||
[named for the MemCachier service it was originally built for][memcachier], it | ||
will work with any memcache server that speaks the binary protocol. | ||
### Custom caching | ||
@@ -244,3 +225,3 @@ | ||
var redisCache = { | ||
var redisStore = { | ||
get: function(key, cb) { | ||
@@ -261,5 +242,6 @@ // Namespace the keys: | ||
var Heroku = require('heroku-client'); | ||
Heroku.configure({ | ||
cache: redisCache | ||
var encryptor = require('simple-encryptor')(SECRET_CACHE_KEY); | ||
var Heroku = require('heroku-client'); | ||
var hk = new Heroku({ | ||
cache: {store: redisStore, encryptor: encryptor} | ||
}); | ||
@@ -266,0 +248,0 @@ ``` |
@@ -5,6 +5,6 @@ 'use strict'; | ||
var encryptor = require('simple-encryptor')(process.env.HEROKU_CLIENT_ENCRYPTION_SECRET); | ||
function MockCache(key) { | ||
this.encryptor = require('simple-encryptor')(key); | ||
} | ||
function MockCache() {} | ||
MockCache.prototype.get = function(key, callback) { | ||
@@ -14,3 +14,3 @@ var body = { cachedFoo: 'bar' }; | ||
value = encryptor.encrypt(value); | ||
value = this.encryptor.encrypt(value); | ||
callback(null, value); | ||
@@ -17,0 +17,0 @@ }; |
'use strict'; | ||
process.env.HEROKU_CLIENT_ENCRYPTION_SECRET = 'abcd1234abcd1234'; | ||
var http = require('http'); | ||
var https = require('https'); | ||
var Request = require('../../lib/request'); | ||
var memjs = require('memjs'); | ||
var MockCache = require('../helpers/mockCache'); | ||
var MockRequest = require('../helpers/mockRequest'); | ||
var MockResponse = require('../helpers/mockResponse'); | ||
var MockCache = require('../helpers/mockCache'); | ||
@@ -62,75 +59,2 @@ describe('request', function() { | ||
describe('when using an HTTP proxy', function() { | ||
beforeEach(function() { | ||
process.env.HEROKU_HTTP_PROXY_HOST='localhost:5000'; | ||
}); | ||
afterEach(function() { | ||
delete process.env.HEROKU_HTTP_PROXY_HOST; | ||
}); | ||
it('uses an http agent', function(done) { | ||
makeRequest('/apps', {}, function() { | ||
expect(http.request.mostRecentCall.args[0].host).toBeDefined(); | ||
done(); | ||
}); | ||
}); | ||
it('uses the proxy host', function(done) { | ||
makeRequest('/apps', {}, function() { | ||
expect(http.request.mostRecentCall.args[0].host).toEqual('localhost:5000'); | ||
done(); | ||
}); | ||
}); | ||
it('uses the full API URL as its path', function(done) { | ||
makeRequest('/apps', {}, function() { | ||
expect(http.request.mostRecentCall.args[0].path).toEqual('https://api.heroku.com/apps'); | ||
done(); | ||
}); | ||
}); | ||
describe('when a proxy port is defined', function() { | ||
beforeEach(function() { | ||
process.env.HEROKU_HTTP_PROXY_PORT='8000'; | ||
}); | ||
afterEach(function() { | ||
delete process.env.HEROKU_HTTP_PROXY_PORT; | ||
}); | ||
it('uses the defined port', function(done) { | ||
makeRequest('/apps', {}, function() { | ||
expect(http.request.mostRecentCall.args[0].port).toEqual('8000'); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe('when a proxy port is not defined', function() { | ||
it('defaults to port 8080', function(done) { | ||
makeRequest('/apps', {}, function() { | ||
expect(http.request.mostRecentCall.args[0].port).toEqual(8080); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('when not using an HTTP proxy', function() { | ||
it('uses the API host as its host', function(done) { | ||
makeRequest('/apps', {}, function() { | ||
expect(https.request.mostRecentCall.args[0].host).toEqual('api.heroku.com'); | ||
done(); | ||
}); | ||
}); | ||
it('makes a request to port 443', function(done) { | ||
makeRequest('/apps', {}, function() { | ||
expect(https.request.mostRecentCall.args[0].port).toEqual(443); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe('callbacks and promises', function() { | ||
@@ -272,13 +196,10 @@ it('sends a successful response to the callback', function(done) { | ||
describe('caching', function() { | ||
var secret = process.env.HEROKU_CLIENT_ENCRYPTION_SECRET; | ||
var encryptor = require('simple-encryptor')(secret); | ||
var cache = new MockCache(); | ||
var key = 'SECRET_CACHE_KEY'; | ||
var cache = { | ||
store: new MockCache(key), | ||
encryptor: require('simple-encryptor')(key) | ||
}; | ||
beforeEach(function() { | ||
spyOn(memjs.Client, 'create').andReturn(cache); | ||
Request.connectCacheClient({ cache: cache, key: secret }); | ||
}); | ||
it('sends an etag from the cache', function(done) { | ||
makeRequest('/apps', {}, function() { | ||
makeRequest('/apps', {cache: cache}, function() { | ||
expect(https.request.mostRecentCall.args[0].headers['If-None-Match']).toEqual('123'); | ||
@@ -290,7 +211,7 @@ done(); | ||
it('gets with a postfix', function(done) { | ||
spyOn(cache, 'get').andCallThrough(); | ||
spyOn(cache.store, 'get').andCallThrough(); | ||
makeRequest('/apps', { token: 'api-token' }, function() { | ||
makeRequest('/apps', { cache: cache, token: 'api-token' }, function() { | ||
var key = JSON.stringify(['/apps', 'id ]..; max=1000', 'api-token']); | ||
expect(cache.get).toHaveBeenCalledWith(encryptor.hmac(key), jasmine.any(Function)); | ||
expect(cache.store.get).toHaveBeenCalledWith(cache.encryptor.hmac(key), jasmine.any(Function)); | ||
done(); | ||
@@ -301,3 +222,3 @@ }); | ||
it('returns a cached body', function(done) { | ||
makeRequest('/apps', {}, function(err, body) { | ||
makeRequest('/apps', {cache: cache}, function(err, body) { | ||
expect(body).toEqual({ cachedFoo: 'bar' }); | ||
@@ -309,5 +230,5 @@ done(); | ||
it('writes to the cache when necessary', function(done) { | ||
spyOn(cache, 'set'); | ||
spyOn(cache.store, 'set'); | ||
makeRequest('/apps', { token: 'api-token' }, function() { | ||
makeRequest('/apps', { cache: cache, token: 'api-token' }, function() { | ||
var expectedKey = JSON.stringify(['/apps', 'id ]..; max=1000', 'api-token']); | ||
@@ -320,4 +241,4 @@ | ||
expect(cache.set).toHaveBeenCalledWith(encryptor.hmac(expectedKey), jasmine.any(String)); | ||
expect(encryptor.decrypt(cache.set.mostRecentCall.args[1])).toEqual(expectedValue); | ||
expect(cache.store.set).toHaveBeenCalledWith(cache.encryptor.hmac(expectedKey), jasmine.any(String)); | ||
expect(cache.encryptor.decrypt(cache.store.set.mostRecentCall.args[1])).toEqual(expectedValue); | ||
done(); | ||
@@ -324,0 +245,0 @@ }, { response: { headers: { etag: '123' } } }); |
{ | ||
"name": "heroku-cli-util", | ||
"version": "5.1.0", | ||
"version": "5.2.0", | ||
"description": "Set of helpful CLI utilities", | ||
@@ -29,3 +29,3 @@ "main": "index.js", | ||
"co": "^4.5.4", | ||
"heroku-client": "^1.11.0" | ||
"heroku-client": "^2.0.0" | ||
}, | ||
@@ -32,0 +32,0 @@ "bundledDependencies": [ |
@@ -28,2 +28,4 @@ # heroku-cli-util | ||
Note: to use `yield` you need to wrap this in a [co](https://github.com/tj/co) block. | ||
## Prompt | ||
@@ -49,3 +51,3 @@ | ||
Generator style (must be wrapped in cli.command() or co block) | ||
Generator style (must be wrapped in a [co](https://github.com/tj/co) block) | ||
@@ -52,0 +54,0 @@ ```js |
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
Network access
Supply chain riskThis module accesses the network.
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 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
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 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Copyleft License
License(Experimental) Copyleft license information was found.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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 7 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
Non-permissive License
License(Experimental) A license not known to be considered permissive was found.
Found 1 instance in 1 package
1
100
216
14
8
652463
117
13602
Updatedheroku-client@^2.0.0