UNPKG

sandboxjs

Version:
1,131 lines (941 loc) 40.9 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; module.exports.PARSE_NEVER = 0; module.exports.PARSE_ALWAYS = 1; module.exports.PARSE_ON_ARITY = 2; /** * 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 * @param {String} [options.onBeforeRequest] - An array of hook functions to be invoked with a prepared request */ function Sandbox (options) { var securityVersion = 'v1'; this.url = options.url; this.container = options.container; this.token = options.token; this.csrfToken = options.csrfToken; this.onBeforeRequest = [] .concat(options.onBeforeRequest) .filter(function(hook) { return typeof hook === 'function' } ); this.headers = { 'Authorization': 'Bearer ' + this.token}; if (this.csrfToken) { this.headers['X-CSRFToken'] = this.csrfToken; } try { var typ = Decode(options.token, { header: true }).typ; if (typ && typ.toLowerCase() === 'jwt') { securityVersion = 'v2'; } } catch (_) { // Ignore jwt decoding failures and assume v1 opaque token } this.securityVersion = securityVersion; } /** * Create a clone of this sandbox instances with one or more different parameters * * @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 * @param {String} [options.onBeforeRequest] - An array of hook functions to be invoked with a prepared request */ Sandbox.prototype.clone = function (options) { return new Sandbox({ url: options.url || this.url, container: options.container || this.container, token: options.token || this.token, onBeforeRequest: options.onBeforeRequest || this.onBeforeRequest, }); }; /** * 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 = typeof options.code === 'string' ? 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 token_options = defaults({}, options, { include_webtask_url: true }); var promise = this.createToken(token_options) .then(function (result) { return token_options.include_webtask_url ? new Webtask(self, result.token, { meta: options.meta, webtask_url: result.webtask_url }) : new Webtask(self, result, { meta: options.meta }); }); 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, { meta: claims.meta }); }); 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.host) params.host = options.host; if (options.code_url) params.url = options.code_url; if (typeof options.code === 'string') 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.meta && Object.keys(options.meta).length > 0) params.meta = options.meta; 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) !== undefined) // This can be a numeric value from the PARSE_* enumeration // or a boolean that will be normalized to 0 or 1. params.pb = +(options.parse || options.parseBody); 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, { include_webtask_url: options.include_webtask_url })); 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; }; /** * Run a prepared Superagent request through any configured * onBeforeRequest hooks. * * This can be useful for enablying proxies for server-side * consumers of sandboxjs. * * @param {Superagent.Request} request - Instance of a superagent request * @param {function} [cb] - Node-style callback function * @returns {Promise} - A promise representing the fulfillment of the request */ Sandbox.prototype.issueRequest = function (request, cb) { request.set(this.headers) const transformedRequest = this.onBeforeRequest .reduce(function(request, hook) { return hook(request, this); }, request); const promise = Request(transformedRequest); 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 {Object} [options] - Optional options. Currently only options.include_webtask_url is supported. * @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, options, cb) { if (typeof options === 'function') { cb = options; options = undefined; } var request = Superagent .post(this.url + '/api/tokens/issue') request.send(claims); var promise = this.issueRequest(request) .then(function (res) { return (options && options.include_webtask_url) ? { token: res.text, webtask_url: res.header['location'] } : res.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=' + encodeURIComponent(this.token); return LogStream(url); }; Sandbox.prototype._createCronJob = function (job) { return new CronJob(this, job); }; /** * Read a named webtask * * @param {Object} options - Options * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container. * @param {String} options.name - The name of the webtask. * @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. * @return {Promise} A Promise that will be fulfilled with an array of Webtasks */ Sandbox.prototype.getWebtask = function (options, cb) { if (!options) options = {}; var promise; if (!options.name) { var err = new Error('Missing required option: `options.name`'); err.statusCode = 400; promise = Bluebird.reject(err); } else { var url = this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name; var request = Superagent .get(url) .accept('json'); var self = this; if (options.decrypt) request.query({ decrypt: options.decrypt }); if (options.fetch_code) request.query({ fetch_code: options.fetch_code }); promise = this.issueRequest(request) .get('body') .then(function (data) { return new Webtask(self, data.token, { name: data.name, secrets: data.secrets, code: data.code, meta: data.meta, webtask_url: data.webtask_url }); }); } return cb ? promise.nodeify(cb) : promise; }; /** * Create a named webtask * * @param {Object} options - Options * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container. * @param {String} options.name - The name of the webtask. * @param {String} [options.secrets] - Set the webtask secrets. * @param {String} [options.meta] - Set the webtask metadata. * @param {String} [options.host] - Set the webtask hostname. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @return {Promise} A Promise that will be fulfilled with an array of Webtasks */ Sandbox.prototype.createWebtask = function (options, cb) { if (!options) options = {}; var promise; var err; if (typeof options.name !== 'string') { err = new Error('The `name` option is required and must be a string'); err.statusCode = 400; promise = Bluebird.reject(err); } if (options.code && typeof options.code !== 'string') { err = new Error('The `code` option must be a string'); err.statusCode = 400; promise = Bluebird.reject(err); } if (options.url && typeof options.url !== 'string') { err = new Error('The `url` option must be a string'); err.statusCode = 400; promise = Bluebird.reject(err); } if (options.code && options.url) { err = new Error('Either the `code` or `url` option can be specified, but not both'); err.statusCode = 400; promise = Bluebird.reject(err); } if (!err) { var url = this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name; var payload = {}; if (options.secrets) payload.secrets = options.secrets; if (options.code) payload.code = options.code; if (options.url) payload.url = options.url; if (options.meta) payload.meta = options.meta; if (options.host) payload.host = options.host; var request = Superagent .put(url) request.send(payload) var self = this; promise = this.issueRequest(request) .then(function (res) { return new Webtask(self, res.body.token, { name: res.body.name, container: options.container || self.container, meta: res.body.meta, webtask_url: res.body.webtask_url }); }); } return cb ? promise.nodeify(cb) : promise; }; /** * Remove a named webtask from the webtask container * * @param {Object} options - Options * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container. * @param {String} options.name - The name of the cron job. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @return {Promise} A Promise that will be fulfilled with an array of Webtasks */ Sandbox.prototype.removeWebtask = function (options, cb) { if (!options) options = {}; var promise; if (!options.name) { var err = new Error('Missing required option: `options.name`'); err.statusCode = 400; promise = Bluebird.reject(err); } else { var url = this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name; var request = Superagent .del(url) promise = this.issueRequest(request) .return(true); } return cb ? promise.nodeify(cb) : promise; }; /** * Update an existing webtask's code, secrets or other claims * * Note that this method should be used with caution as there is the potential * for a race condition where another agent updates the webtask between the time * that the webtask details and claims are resolved and when the webtask * update is issued. * * @param {Object} options - Options * @param {String} options.name - Name of the webtask to update * @param {String} [options.code] - Updated code for the webtask * @param {String} [options.url] - Updated code URL for the webtask * @param {String} [options.secrets] - If `false`, remove existing secrets, if an object update secrets, otherwise preserve * @param {String} [options.params] - If `false`, remove existing params, if an object update params, otherwise preserve * @param {String} [options.host] - If `false`, remove existing host, if a string update host, otherwise preserve * @param {Function} [cb] - Optional callback function for node-style callbacks. * @return {Promise} A Promise that will be fulfilled with an instance of Webtask representing the updated webtask */ Sandbox.prototype.updateWebtask = function (options, cb) { if (!options) options = {}; var self = this; var err; var promise; if (typeof options.name !== 'string') { err = new Error('The `name` option is required and must be a string'); err.statusCode = 400; promise = Bluebird.reject(err); } if (options.code && typeof options.code !== 'string') { err = new Error('The `code` option must be a string'); err.statusCode = 400; promise = Bluebird.reject(err); } if (options.url && typeof options.url !== 'string') { err = new Error('The `url` option must be a string'); err.statusCode = 400; promise = Bluebird.reject(err); } if (options.code && options.url) { err = new Error('Either the `code` or `url` option can be specified, but not both'); err.statusCode = 400; promise = Bluebird.reject(err); } if (!err) { promise = this.getWebtask({ name: options.name }) .then(onWebtask); } return cb ? promise.nodeify(cb) : promise; function onWebtask(webtask) { return webtask.inspect({ decrypt: options.secrets !== false, fetch_code: options.code !== false, meta: options.meta !== false }) .then(onInspection); function onInspection(claims) { var newClaims = { ten: claims.ten, jtn: claims.jtn, }; ['nbf', 'exp', 'dd', 'dr', 'ls', 'lm', 'lh', 'ld', 'lw', 'lo', 'lts', 'ltm', 'lth', 'ltd', 'ltw', 'lto'] .forEach(function (claim) { if (claims[claim]) { newClaims[claim] = claims[claim]; } }); if (options.host !== false && (options.host || claims.host)) { newClaims.host = options.host || claims.host; } if (typeof options.parseBody !== 'undefined' || typeof options.parse !== 'undefined' || claims.pb) { newClaims.pb = +(options.parseBody || options.parse || claims.pb); } if (typeof options.mergeBody !== 'undefined' || typeof options.merge !== 'undefined' || claims.mb) { newClaims.mb = +(options.mergeBody || options.merge || claims.mb); } if (options.secrets !== false && (options.secrets || claims.ectx)) { newClaims.ectx = options.secrets || claims.ectx; } if (options.params !== false && (options.params || claims.pctx)) { newClaims.pctx = options.params || claims.pctx; } if (options.meta !== false && (options.meta || claims.meta)) { newClaims.meta = options.meta || claims.meta; } if (options.url) { newClaims.url = options.url; } if (options.code) { newClaims.code = options.code; } return self.createRaw(newClaims, { include_webtask_url: options.include_webtask_url }); } } }; /** * List named webtasks from the webtask container * * @param {Object} options - Options * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container. * @param {Boolean} [options.fetch_code] - Include the webtask's code in the listing response. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @return {Promise} A Promise that will be fulfilled with an array of Webtasks */ Sandbox.prototype.listWebtasks = function (options, cb) { if (!options) options = {}; var url = this.url + '/api/webtask/' + (options.container || this.container); var request = Superagent .get(url) .accept('json'); if (options.offset) request.query({ offset: options.offset }); if (options.limit) request.query({ limit: options.limit }); if (options.meta) { for (var m in options.meta) { request.query({ meta: m + ':' + options.meta[m] }); } } if (options.fetch_code) request.query({ fetch_code: true }); var self = this; var promise = this.issueRequest(request) .get('body') .map(function (webtask) { return new Webtask(self, webtask.token, { code: webtask.code, name: webtask.name, meta: webtask.meta, webtask_url: webtask.webtask_url, }); }); return cb ? promise.nodeify(cb) : promise; }; /** * Create a cron job from an already-existing webtask * * @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 {String} options.tz - The cron timezone (IANA timezone). * @param {String} options.meta - The cron metadata (set of string key value pairs). * @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 = { schedule: options.schedule, }; if (options.token) payload.token = options.token; if (options.state) { payload.state = options.state; } if (options.meta) { payload.meta = options.meta; } if (options.tz) { payload.tz = options.tz; } var request = Superagent .put(this.url + '/api/cron/' + options.container + '/' + options.name) request.send(payload) .accept('json'); var promise = this.issueRequest(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) request.accept('json'); var promise = this.issueRequest(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') request.send({ state: options.state, }) .accept('json'); var promise = this.issueRequest(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) .accept('json'); if (options.offset) request.query({ offset: options.offset }); if (options.limit) request.query({ limit: options.limit }); if (options.meta) { for (var m in options.meta) { request.query({ meta: m + ':' + options.meta[m] }); } } var promise = this.issueRequest(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) .accept('json'); var promise = this.issueRequest(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') .accept('json'); if (options.offset) request.query({offset: options.offset}); if (options.limit) request.query({limit: options.limit}); var promise = this.issueRequest(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 (__) { // Do nothing } } } 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 }) .accept('json'); if (options.decrypt) request.query({ decrypt: options.decrypt }); if (options.fetch_code) request.query({ fetch_code: options.fetch_code }); if (options.meta) request.query({ meta: options.meta }); var promise = this.issueRequest(request) .get('body'); return cb ? promise.nodeify(cb) : promise; }; /** * Inspect an existing named webtask to resolve code and/or secrets * * @param {Object} options - Options for inspecting the webtask. * @param {Boolean} options.name - The named webtask 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.inspectWebtask = function (options, cb) { options = defaults(options, { container: this.container }); var promise = this.securityVersion === 'v2' ? this.getWebtask({ name: options.name, decrypt: options.decrypt, fetch_code: options.fetch_code }) : this.getWebtask({ name: options.name }) .call('inspect', { decrypt: options.decrypt, fetch_code: options.fetch_code, meta: options.meta }); 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') request.query({ token: token }); var promise = this.issueRequest(request); return cb ? promise.nodeify(cb) : promise; }; /** * List versions of a given node module that are available on the platform * * @param {Object} options - Options * @param {String} options.name - Name of the node module * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with the token */ Sandbox.prototype.listNodeModuleVersions = function (options, cb) { var request = Superagent .get(this.url + `/api/env/node/modules/${encodeURIComponent(options.name)}`); var promise = this.issueRequest(request) .get('body'); return cb ? promise.nodeify(cb) : promise; }; /** * Ensure that a set of modules are available on the platform * * @param {Object} options - Options * @param {Array} options.modules - Array of { name, version } pairs * @param {Boolean} options.reset - Trigger a rebuild of the modules (Requires administrative token) * @param {Function} [cb] - Optional callback function for node-style callbacks * @returns {Promise} A Promise that will be fulfilled with an array of { name, version, state } objects */ Sandbox.prototype.ensureNodeModules = function (options, cb) { var request = Superagent .post(this.url + '/api/env/node/modules') .send({ modules: options.modules }) if (options.reset) { request.query({ reset: 1 }); } var promise = this.issueRequest(request) .get('body'); return cb ? promise.nodeify(cb) : promise; }; /** * Update the storage associated to the a webtask * * @param {Object} options - Options * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container. * @param {String} options.name - The name of the webtask. * @param {Object} storage - storage * @param {Object} storage.data - The data to be stored * @param {String} storage.etag - Pass in an optional string to be used for optimistic concurrency control to prevent simultaneous updates of the same data. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @return {Promise} A Promise that will be fulfilled with an array of Webtasks */ Sandbox.prototype.updateStorage = function (storage, options, cb) { var promise; if (!options || !options.name) { var err = new Error('Missing required option: `options.name`'); err.statusCode = 400; promise = Bluebird.reject(err); } else { var obj = { data: JSON.stringify(storage.data) } if (storage.etag) { obj.etag = storage.etag.toString(); } var request = Superagent .put(this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name + '/data') .send(obj) .accept('json'); promise = this.issueRequest(request) .get('body'); } return cb ? promise.nodeify(cb) : promise; }; /** * Read the storage associated to the a webtask * * @param {Object} options - Options * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container. * @param {String} options.name - The name of the webtask. * @param {Function} [cb] - Optional callback function for node-style callbacks. * @return {Promise} A Promise that will be fulfilled with an array of Webtasks */ Sandbox.prototype.getStorage = function (options, cb) { var promise; if (!options || !options.name) { var err = new Error('Missing required option: `options.name`'); err.statusCode = 400; promise = Bluebird.reject(err); } else { var request = Superagent .get(this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name + '/data') .accept('json'); promise = this.issueRequest(request) .get('body') .then(function (result) { var storage = result; try { storage.data = JSON.parse(result.data); } catch(e) { // TODO: Log somewhere } return storage; }); } 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 'sandbox.auth0-extend.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 'sandbox.auth0-extend.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 + '`.'); defaults(options, { url: 'https://sandbox.auth0-extend.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) { 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, }; };