UNPKG

sandboxjs

Version:
590 lines (488 loc) 21 kB
var Bluebird = require('bluebird'); var CronJob = require('./cronJob'); var Decode = require('jwt-decode'); var LogStream = require('webtask-log-stream'); var RandExp = require('randexp'); var Request = require('./issueRequest'); var Superagent = require('superagent'); var Webtask = require('./webtask'); var defaults = require('lodash.defaults'); /** Sandbox node.js code. @module sandboxjs @typicalname Sandbox */ module.exports = Sandbox; /** * Creates an object representing a user's webtask.io credentials * * @constructor * @param {Object} options - Options used to configure the profile * @param {String} options.url - The url of the webtask cluster where code will run * @param {String} options.container - The name of the container in which code will run * @param {String} options.token - The JWT (see: http://jwt.io) issued by webtask.io that grants rights to run code in the indicated container */ function Sandbox (options) { this.url = options.url; this.container = options.container; this.token = options.token; } /** * Create a Webtask from the given options * * @param {String} [codeOrUrl] - The code for the webtask or a url starting with http:// or https:// * @param {Object} [options] - Options for creating the webtask * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with the token */ Sandbox.prototype.create = function (codeOrUrl, options, cb) { if (typeof codeOrUrl !== 'string') { cb = options; options = codeOrUrl; codeOrUrl = options.code || options.code_url; } if (typeof options === 'function') { cb = options; options = {}; } if (!options) options = {}; var fol = codeOrUrl.toLowerCase(); if (fol.indexOf('http://') === 0 || fol.indexOf('https://') === 0) { options.code_url = codeOrUrl; } else { options.code = codeOrUrl; } var self = this; var promise = this.createToken(options) .then(function (token) { return new Webtask(self, token); }); return cb ? promise.nodeify(cb) : promise; }; /** * Create a Webtask from the given claims * * @param {Object} claims - Options for creating the webtask * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with the token */ Sandbox.prototype.createRaw = function (claims, cb) { var self = this; var promise = this.createTokenRaw(claims) .then(function (token) { return new Webtask(self, token); }); return cb ? promise.nodeify(cb) : promise; }; /** * Shortcut to create a Webtask and get its url from the given options * * @param {Object} options - Options for creating the webtask * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with the token */ Sandbox.prototype.createUrl = function (options, cb) { var promise = this.create(options) .get('url'); return cb ? promise.nodeify(cb) : promise; }; /** * Shortcut to create and run a Webtask from the given options * * @param {String} [codeOrUrl] - The code for the webtask or a url starting with http:// or https:// * @param {Object} [options] - Options for creating the webtask * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with the token */ Sandbox.prototype.run = function (codeOrUrl, options, cb) { if (typeof options === 'function') { cb = options; options = {}; } if (!options) options = {}; var promise = this.create(codeOrUrl, options) .call('run', options); return cb ? promise.nodeify(cb, {spread: true}) : promise; }; /** * Create a webtask token - A JWT (see: http://jwt.io) with the supplied options * * @param {Object} options - Claims to make for this token (see: https://webtask.io/docs/api_issue) * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with the token */ Sandbox.prototype.createToken = function (options, cb) { if (!options) options = {}; var self = this; var promise = new Bluebird(function (resolve, reject) { var params = { ten: options.container || self.container, dd: options.issuanceDepth || 0, }; if (options.exp !== undefined && options.nbf !== undefined && options.exp <= options.nbf) { return reject('The `nbf` parameter cannot be set to a later time than `exp`.'); } if (options.code_url) params.url = options.code_url; if (options.code) params.code = options.code; if (options.secrets && Object.keys(options.secrets).length > 0) params.ectx = options.secrets; if (options.secret && Object.keys(options.secret).length > 0) params.ectx = options.secret; if (options.params && Object.keys(options.params).length > 0) params.pctx = options.params; if (options.param && Object.keys(options.param).length > 0) params.pctx = options.param; if (options.nbf !== undefined) params.nbf = options.nbf; if (options.exp !== undefined) params.exp = options.exp; if (options.merge || options.mergeBody) params.mb = 1; if (options.parse || options.parseBody) params.pb = 1; if (!options.selfRevoke) params.dr = 1; if (options.name) params.jtn = options.name; try { if (options.tokenLimit) addLimits(options.tokenLimit, Sandbox.limits.token); if (options.containerLimit) addLimits(options.containerLimit, Sandbox.limits.container); } catch (err) { return reject(err); } return resolve(self.createTokenRaw(params)); function addLimits(limits, spec) { for (var l in limits) { var limit = parseInt(limits[l], 10); if (!spec[l]) { throw new Error('Unsupported limit type `' + l + '`. Supported limits are: ' + Object.keys(spec).join(', ') + '.'); } if (isNaN(limits[l]) || Math.floor(+limits[l]) !== limit || limit < 1) { throw new Error('Unsupported limit value for `' + l + '` limit. All limits must be positive integers.'); } params[spec[l]] = limit; } } }); return cb ? promise.nodeify(cb) : promise; }; /** * Create a webtask token - A JWT (see: http://jwt.io) with the supplied claims * * @param {Object} claims - Claims to make for this token (see: https://webtask.io/docs/api_issue) * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with the token */ Sandbox.prototype.createTokenRaw = function (claims, cb) { var request = Superagent .post(this.url + '/api/tokens/issue') .set('Authorization', 'Bearer ' + this.token) .send(claims); var promise = Request(request) .get('text'); return cb ? promise.nodeify(cb) : promise; }; /** * Create a stream of logs from the webtask container * * Note that the logs will include messages from our infrastructure. * * @param {Object} options - Streaming options overrides * @param {String} [options.container] - The container for which you would like to stream logs. Defaults to the current profile's container. * @returns {Stream} A stream that will emit 'data' events with container logs */ Sandbox.prototype.createLogStream = function (options) { if (!options) options = {}; var url = this.url + '/api/logs/tenant/' + (options.container || this.container) + '?key=' + this.token; return LogStream(url); }; Sandbox.prototype._createCronJob = function (job) { return new CronJob(this, job); }; /** * Create a cron job from an already-existing webtask token * * @param {Object} options - Options for creating a cron job * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container. * @param {String} options.name - The name of the cron job. * @param {String} options.token - The webtask token that will be used to run the job. * @param {String} options.schedule - The cron schedule that will be used to determine when the job will be run. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @returns {Promise} A Promise that will be fulfilled with a {@see CronJob} instance. */ Sandbox.prototype.createCronJob = function (options, cb) { options = defaults(options, { container: this.container }); var payload = { token: options.token, schedule: options.schedule, }; if (options.state) { payload.state = options.state; } var request = Superagent .put(this.url + '/api/cron/' + options.container + '/' + options.name) .set('Authorization', 'Bearer ' + this.token) .send(payload) .accept('json'); var promise = Request(request) .get('body') .then(this._createCronJob.bind(this)); return cb ? promise.nodeify(cb) : promise; }; /** * Remove an existing cron job * * @param {Object} options - Options for removing the cron job * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container. * @param {String} options.name - The name of the cron job. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @returns {Promise} A Promise that will be fulfilled with the response from removing the job. */ Sandbox.prototype.removeCronJob = function (options, cb) { options = defaults(options, { container: this.container }); var request = Superagent .del(this.url + '/api/cron/' + options.container + '/' + options.name) .set('Authorization', 'Bearer ' + this.token) .accept('json'); var promise = Request(request) .get('body'); return cb ? promise.nodeify(cb) : promise; }; /** * Set an existing cron job's state * * @param {Object} options - Options for updating the cron job's state * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container. * @param {String} options.name - The name of the cron job. * @param {String} options.state - The new state of the cron job. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @returns {Promise} A Promise that will be fulfilled with the response from removing the job. */ Sandbox.prototype.setCronJobState = function (options, cb) { options = defaults(options, { container: this.container }); var request = Superagent .put(this.url + '/api/cron/' + options.container + '/' + options.name + '/state') .set('Authorization', 'Bearer ' + this.token) .send({ state: options.state, }) .accept('json'); var promise = Request(request) .get('body'); return cb ? promise.nodeify(cb) : promise; }; /** * List cron jobs associated with this profile * * @param {Object} [options] - Options for listing cron jobs. * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @returns {Promise} A Promise that will be fulfilled with an Array of {@see CronJob} instances. */ Sandbox.prototype.listCronJobs = function (options, cb) { if (typeof options === 'function') { cb = options; options = null; } if (!options) options = {}; options = defaults(options, { container: this.container }); var request = Superagent .get(this.url + '/api/cron/' + options.container) .set('Authorization', 'Bearer ' + this.token) .accept('json'); var promise = Request(request) .get('body') .map(this._createCronJob.bind(this)); return cb ? promise.nodeify(cb) : promise; }; /** * Get a CronJob instance associated with an existing cron job * * @param {Object} options - Options for retrieving the cron job. * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container. * @param {String} options.name - The name of the cron job. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @returns {Promise} A Promise that will be fulfilled with a {@see CronJob} instance. */ Sandbox.prototype.getCronJob = function (options, cb) { options = defaults(options, { container: this.container }); var request = Superagent .get(this.url + '/api/cron/' + options.container + '/' + options.name) .set('Authorization', 'Bearer ' + this.token) .accept('json'); var promise = Request(request) .get('body') .then(this._createCronJob.bind(this)); return cb ? promise.nodeify(cb) : promise; }; /** * Get the historical results of executions of an existing cron job. * * @param {Object} options - Options for retrieving the cron job. * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container. * @param {String} options.name - The name of the cron job. * @param {String} [options.offset] - The offset to use when paging through results. * @param {String} [options.limit] - The limit to use when paging through results. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @returns {Promise} A Promise that will be fulfilled with an Array of cron job results. */ Sandbox.prototype.getCronJobHistory = function (options, cb) { options = defaults(options, { container: this.container }); var request = Superagent .get(this.url + '/api/cron/' + options.container + '/' + options.name + '/history') .set('Authorization', 'Bearer ' + this.token) .accept('json'); if (options.offset) request.query({offset: options.offset}); if (options.limit) request.query({limit: options.limit}); var promise = Request(request) .get('body') .map(function (result) { var auth0HeaderRx = /^x-auth0/; result.scheduled_at = new Date(result.scheduled_at); result.started_at = new Date(result.started_at); result.completed_at = new Date(result.completed_at); for (var header in result.headers) { if (auth0HeaderRx.test(header)) { try { result.headers[header] = JSON.parse(result.headers[header]); } catch (__) {} } } return result; }); return cb ? promise.nodeify(cb) : promise; }; /** * Inspect an existing webtask token to resolve code and/or secrets * * @param {Object} options - Options for inspecting the webtask. * @param {Boolean} options.token - The token that you would like to inspect. * @param {Boolean} [options.decrypt] - Decrypt the webtask's secrets. * @param {Boolean} [options.fetch_code] - Fetch the code associated with the webtask. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @returns {Promise} A Promise that will be fulfilled with the resolved webtask data. */ Sandbox.prototype.inspectToken = function (options, cb) { options = defaults(options, { container: this.container }); var request = Superagent .get(this.url + '/api/tokens/inspect') .query({ token: options.token }) .set('Authorization', 'Bearer ' + this.token) .accept('json'); if (options.decrypt) request.query({ decrypt: options.decrypt }); if (options.fetch_code) request.query({ fetch_code: options.fetch_code }); var promise = Request(request) .get('body'); return cb ? promise.nodeify(cb) : promise; }; /** * Revoke a webtask token * * @param {String} token - The token that should be revoked * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with the token * @see https://webtask.io/docs/api_revoke */ Sandbox.prototype.revokeToken = function (token, cb) { var request = Superagent .post(this.url + '/api/tokens/revoke') .set('Authorization', 'Bearer ' + this.token) .query({ token: token }); var promise = Request(request); return cb ? promise.nodeify(cb) : promise; }; Sandbox.limits = { container: { second: 'ls', minute: 'lm', hour: 'lh', day: 'ld', week: 'lw', month: 'lo' }, token: { second: 'lts', minute: 'ltm', hour: 'lth', day: 'ltd', week: 'ltw', month: 'lto', }, }; /** * Create a Sandbox instance from a webtask token * * @param {String} token - The webtask token from which the Sandbox profile will be derived. * @param {Object} options - The options for creating the Sandbox instance that override the derived values from the token. * @param {String} [options.url] - The url of the webtask cluster. Defaults to the public 'webtask.it.auth0.com' cluster. * @param {String} options.container - The container with which this Sandbox instance should be associated. Note that your Webtask token must give you access to that container or all operations will fail. * @param {String} options.token - The Webtask Token. See: https://webtask.io/docs/api_issue. * @returns {Sandbox} A {@see Sandbox} instance whose url, token and container were derived from the given webtask token. * * @alias module:sandboxjs.fromToken */ Sandbox.fromToken = function (token, options) { var config = defaults({}, options, Sandbox.optionsFromJwt(token)); return Sandbox.init(config); }; /** * Create a Sandbox instance * * @param {Object} options - The options for creating the Sandbox instance. * @param {String} [options.url] - The url of the webtask cluster. Defaults to the public 'webtask.it.auth0.com' cluster. * @param {String} options.container - The container with which this Sandbox instance should be associated. Note that your Webtask token must give you access to that container or all operations will fail. * @param {String} options.token - The Webtask Token. See: https://webtask.io/docs/api_issue. * @returns {Sandbox} A {@see Sandbox} instance. * * @alias module:sandboxjs.init */ Sandbox.init = function (options) { if (typeof options !== 'object') throw new Error('Expecting an options Object, got `' + typeof options + '`.'); if (!options.container) throw new Error('A Sandbox instance cannot be created without a container.'); if (typeof options.container !== 'string') throw new Error('Only String containers are supported, got `' + typeof options.container + '`.'); if (!options.token) throw new Error('A Sandbox instance cannot be created without a token.'); defaults(options, { url: 'https://webtask.it.auth0.com', }); return new Sandbox(options); }; Sandbox.optionsFromJwt = function (jwt) { var claims = Decode(jwt); if (!claims) throw new Error('Unable to decode token `' + jwt + '` (https://jwt.io/#id_token=' + jwt + ').'); // What does the var ten = claims.ten; if (!ten) throw new Error('Invalid token, missing `ten` claim `' + jwt + '` (https://jwt.io/#id_token=' + jwt + ').'); if (Array.isArray(ten)) { ten = ten[0]; } else { // Check if the `ten` claim is a RegExp var matches = ten.match(/\/(.+)\//); if (matches) { try { var regex = new RegExp(matches[1]); var gen = new RandExp(regex); // Monkey-patch RandExp to be deterministic gen.randInt = function (l, h) { return l; }; ten = gen.gen(); } catch (err) { throw new Error('Unable to derive containtainer name from `ten` claim `' + claims.ten + '`: ' + err.message + '.'); } } } if (typeof ten !== 'string' || !ten) throw new Error('Expecting `ten` claim to be a non-blank string, got `' + typeof ten + '`, with value `' + ten + '`.'); return { container: ten, token: jwt, }; };