UNPKG

41.3 kBJavaScriptView Raw
1var Bluebird = require('bluebird');
2var CronJob = require('./cronJob');
3var Decode = require('jwt-decode');
4var LogStream = require('webtask-log-stream');
5var RandExp = require('randexp');
6var Request = require('./issueRequest');
7var Superagent = require('superagent');
8var Webtask = require('./webtask');
9
10var defaults = require('lodash.defaults');
11
12
13/**
14Sandbox node.js code.
15@module sandboxjs
16@typicalname Sandbox
17*/
18module.exports = Sandbox;
19module.exports.PARSE_NEVER = 0;
20module.exports.PARSE_ALWAYS = 1;
21module.exports.PARSE_ON_ARITY = 2;
22
23
24
25/**
26 * Creates an object representing a user's webtask.io credentials
27 *
28 * @constructor
29 * @param {Object} options - Options used to configure the profile
30 * @param {String} options.url - The url of the webtask cluster where code will run
31 * @param {String} options.container - The name of the container in which code will run
32 * @param {String} options.token - The JWT (see: http://jwt.io) issued by webtask.io that grants rights to run code in the indicated container
33 * @param {String} [options.onBeforeRequest] - An array of hook functions to be invoked with a prepared request
34 */
35function Sandbox (options) {
36 var securityVersion = 'v1';
37
38 this.url = options.url;
39 this.container = options.container;
40 this.token = options.token;
41 this.onBeforeRequest = []
42 .concat(options.onBeforeRequest)
43 .filter(hook => typeof hook === 'function');
44
45 try {
46 var typ = Decode(options.token, { header: true }).typ;
47
48 if (typ && typ.toLowerCase() === 'jwt') {
49 securityVersion = 'v2';
50 }
51 } catch (_) {
52 // Ignore jwt decoding failures and assume v1 opaque token
53 }
54
55 this.securityVersion = securityVersion;
56}
57
58/**
59 * Create a clone of this sandbox instances with one or more different parameters
60 *
61 * @param {Object} options - Options used to configure the profile
62 * @param {String} [options.url] - The url of the webtask cluster where code will run
63 * @param {String} [options.container] - The name of the container in which code will run
64 * @param {String} [options.token] - The JWT (see: http://jwt.io) issued by webtask.io that grants rights to run code in the indicated container
65 * @param {String} [options.onBeforeRequest] - An array of hook functions to be invoked with a prepared request
66 */
67Sandbox.prototype.clone = function (options) {
68 return new Sandbox({
69 url: options.url || this.url,
70 container: options.container || this.container,
71 token: options.token || this.token,
72 onBeforeRequest: options.onBeforeRequest || this.onBeforeRequest,
73 });
74};
75
76/**
77 * Create a Webtask from the given options
78 *
79 * @param {String} [codeOrUrl] - The code for the webtask or a url starting with http:// or https://
80 * @param {Object} [options] - Options for creating the webtask
81 * @param {Function} [cb] - Optional callback function for node-style callbacks
82 * @returns {Promise} A Promise that will be fulfilled with the token
83 */
84Sandbox.prototype.create = function (codeOrUrl, options, cb) {
85 if (typeof codeOrUrl !== 'string') {
86 cb = options;
87 options = codeOrUrl;
88 codeOrUrl = typeof options.code === 'string'
89 ? options.code
90 : options.code_url;
91 }
92
93 if (typeof options === 'function') {
94 cb = options;
95 options = {};
96 }
97
98 if (!options) options = {};
99
100 var fol = codeOrUrl.toLowerCase();
101
102 if (fol.indexOf('http://') === 0 || fol.indexOf('https://') === 0) {
103 options.code_url = codeOrUrl;
104 } else {
105 options.code = codeOrUrl;
106 }
107
108 var self = this;
109 var token_options = defaults({}, options, { include_webtask_url: true });
110 var promise = this.createToken(token_options)
111 .then(function (result) {
112 return token_options.include_webtask_url
113 ? new Webtask(self, result.token, { meta: options.meta, webtask_url: result.webtask_url })
114 : new Webtask(self, result, { meta: options.meta });
115 });
116
117 return cb ? promise.nodeify(cb) : promise;
118};
119
120/**
121 * Create a Webtask from the given claims
122 *
123 * @param {Object} claims - Options for creating the webtask
124 * @param {Function} [cb] - Optional callback function for node-style callbacks
125 * @returns {Promise} A Promise that will be fulfilled with the token
126 */
127Sandbox.prototype.createRaw = function (claims, cb) {
128 var self = this;
129
130 var promise = this.createTokenRaw(claims)
131 .then(function (token) {
132 return new Webtask(self, token, { meta: claims.meta });
133 });
134
135 return cb ? promise.nodeify(cb) : promise;
136};
137
138/**
139 * Shortcut to create a Webtask and get its url from the given options
140 *
141 * @param {Object} options - Options for creating the webtask
142 * @param {Function} [cb] - Optional callback function for node-style callbacks
143 * @returns {Promise} A Promise that will be fulfilled with the token
144 */
145Sandbox.prototype.createUrl = function (options, cb) {
146 var promise = this.create(options)
147 .get('url');
148
149 return cb ? promise.nodeify(cb) : promise;
150};
151
152/**
153 * Shortcut to create and run a Webtask from the given options
154 *
155 * @param {String} [codeOrUrl] - The code for the webtask or a url starting with http:// or https://
156 * @param {Object} [options] - Options for creating the webtask
157 * @param {Function} [cb] - Optional callback function for node-style callbacks
158 * @returns {Promise} A Promise that will be fulfilled with the token
159 */
160Sandbox.prototype.run = function (codeOrUrl, options, cb) {
161 if (typeof options === 'function') {
162 cb = options;
163 options = {};
164 }
165
166 if (!options) options = {};
167
168 var promise = this.create(codeOrUrl, options)
169 .call('run', options);
170
171 return cb ? promise.nodeify(cb, {spread: true}) : promise;
172};
173
174/**
175 * Create a webtask token - A JWT (see: http://jwt.io) with the supplied options
176 *
177 * @param {Object} options - Claims to make for this token (see: https://webtask.io/docs/api_issue)
178 * @param {Function} [cb] - Optional callback function for node-style callbacks
179 * @returns {Promise} A Promise that will be fulfilled with the token
180 */
181Sandbox.prototype.createToken = function (options, cb) {
182 if (!options) options = {};
183
184 var self = this;
185 var promise = new Bluebird(function (resolve, reject) {
186 var params = {
187 ten: options.container || self.container,
188 dd: options.issuanceDepth || 0,
189 };
190
191 if (options.exp !== undefined && options.nbf !== undefined
192 && options.exp <= options.nbf) {
193 return reject('The `nbf` parameter cannot be set to a later time than `exp`.');
194 }
195
196 if (options.host)
197 params.host = options.host;
198 if (options.code_url)
199 params.url = options.code_url;
200 if (typeof options.code === 'string')
201 params.code = options.code;
202 if (options.secrets && Object.keys(options.secrets).length > 0)
203 params.ectx = options.secrets;
204 if (options.secret && Object.keys(options.secret).length > 0)
205 params.ectx = options.secret;
206 if (options.params && Object.keys(options.params).length > 0)
207 params.pctx = options.params;
208 if (options.param && Object.keys(options.param).length > 0)
209 params.pctx = options.param;
210 if (options.meta && Object.keys(options.meta).length > 0)
211 params.meta = options.meta;
212 if (options.nbf !== undefined)
213 params.nbf = options.nbf;
214 if (options.exp !== undefined)
215 params.exp = options.exp;
216 if (options.merge || options.mergeBody)
217 params.mb = 1;
218 if ((options.parse || options.parseBody) !== undefined)
219 // This can be a numeric value from the PARSE_* enumeration
220 // or a boolean that will be normalized to 0 or 1.
221 params.pb = +(options.parse || options.parseBody);
222 if (!options.selfRevoke)
223 params.dr = 1;
224 if (options.name)
225 params.jtn = options.name;
226
227 try {
228 if (options.tokenLimit)
229 addLimits(options.tokenLimit, Sandbox.limits.token);
230 if (options.containerLimit)
231 addLimits(options.containerLimit, Sandbox.limits.container);
232 } catch (err) {
233 return reject(err);
234 }
235
236 return resolve(self.createTokenRaw(params, { include_webtask_url: options.include_webtask_url }));
237
238 function addLimits(limits, spec) {
239 for (var l in limits) {
240 var limit = parseInt(limits[l], 10);
241
242 if (!spec[l]) {
243 throw new Error('Unsupported limit type `' + l
244 + '`. Supported limits are: '
245 + Object.keys(spec).join(', ') + '.');
246 }
247
248 if (isNaN(limits[l]) || Math.floor(+limits[l]) !== limit
249 || limit < 1) {
250 throw new Error('Unsupported limit value for `' + l
251 + '` limit. All limits must be positive integers.');
252 }
253
254 params[spec[l]] = limit;
255 }
256 }
257 });
258
259 return cb ? promise.nodeify(cb) : promise;
260};
261
262/**
263 * Run a prepared Superagent request through any configured
264 * onBeforeRequest hooks.
265 *
266 * This can be useful for enablying proxies for server-side
267 * consumers of sandboxjs.
268 *
269 * @param {Superagent.Request} request - Instance of a superagent request
270 * @param {function} [cb] - Node-style callback function
271 * @returns {Promise} - A promise representing the fulfillment of the request
272 */
273Sandbox.prototype.issueRequest = function (request, cb) {
274 const transformedRequest = this.onBeforeRequest
275 .reduce((request, hook) => {
276 return hook(request, this);
277 }, request);
278 const promise = Request(transformedRequest);
279
280 return cb ? promise.nodeify(cb) : promise;
281};
282
283
284/**
285 * Create a webtask token - A JWT (see: http://jwt.io) with the supplied claims
286 *
287 * @param {Object} claims - Claims to make for this token (see: https://webtask.io/docs/api_issue)
288 * @param {Object} [options] - Optional options. Currently only options.include_webtask_url is supported.
289 * @param {Function} [cb] - Optional callback function for node-style callbacks
290 * @returns {Promise} A Promise that will be fulfilled with the token
291 */
292Sandbox.prototype.createTokenRaw = function (claims, options, cb) {
293 if (typeof options === 'function') {
294 cb = options;
295 options = undefined;
296 }
297 var request = Superagent
298 .post(this.url + '/api/tokens/issue')
299 .set('Authorization', 'Bearer ' + this.token)
300 .send(claims);
301
302 var promise = this.issueRequest(request)
303 .then(function (res) {
304 return (options && options.include_webtask_url)
305 ? { token: res.text, webtask_url: res.header['location'] }
306 : res.text;
307 });
308
309 return cb ? promise.nodeify(cb) : promise;
310};
311
312/**
313 * Create a stream of logs from the webtask container
314 *
315 * Note that the logs will include messages from our infrastructure.
316 *
317 * @param {Object} options - Streaming options overrides
318 * @param {String} [options.container] - The container for which you would like to stream logs. Defaults to the current profile's container.
319 * @returns {Stream} A stream that will emit 'data' events with container logs
320 */
321Sandbox.prototype.createLogStream = function (options) {
322 if (!options) options = {};
323
324 var url = this.url + '/api/logs/tenant/'
325 + (options.container || this.container)
326 + '?key=' + encodeURIComponent(this.token);
327
328 return LogStream(url);
329};
330
331Sandbox.prototype._createCronJob = function (job) {
332 return new CronJob(this, job);
333};
334
335/**
336 * Read a named webtask
337 *
338 * @param {Object} options - Options
339 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
340 * @param {String} options.name - The name of the webtask.
341 * @param {Boolean} [options.decrypt] - Decrypt the webtask's secrets.
342 * @param {Boolean} [options.fetch_code] - Fetch the code associated with the webtask.
343 * @param {Function} [cb] - Optional callback function for node-style callbacks.
344 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
345 */
346Sandbox.prototype.getWebtask = function (options, cb) {
347 if (!options) options = {};
348
349 var promise;
350
351 if (!options.name) {
352 var err = new Error('Missing required option: `options.name`');
353 err.statusCode = 400;
354
355 promise = Bluebird.reject(err);
356 } else {
357 var url = this.url + '/api/webtask/'
358 + (options.container || this.container) + '/' + options.name;
359 var request = Superagent
360 .get(url)
361 .set('Authorization', 'Bearer ' + this.token)
362 .accept('json');
363 var self = this;
364
365 if (options.decrypt) request.query({ decrypt: options.decrypt });
366 if (options.fetch_code) request.query({ fetch_code: options.fetch_code });
367
368 promise = this.issueRequest(request)
369 .get('body')
370 .then(function (data) {
371 return new Webtask(self, data.token, { name: data.name, secrets: data.secrets, code: data.code, meta: data.meta, webtask_url: data.webtask_url });
372 });
373 }
374
375 return cb ? promise.nodeify(cb) : promise;
376};
377
378/**
379 * Create a named webtask
380 *
381 * @param {Object} options - Options
382 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
383 * @param {String} options.name - The name of the webtask.
384 * @param {String} [options.secrets] - Set the webtask secrets.
385 * @param {String} [options.meta] - Set the webtask metadata.
386 * @param {String} [options.host] - Set the webtask hostname.
387 * @param {Function} [cb] - Optional callback function for node-style callbacks.
388 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
389 */
390Sandbox.prototype.createWebtask = function (options, cb) {
391 if (!options) options = {};
392
393 var promise;
394 var err;
395
396 if (typeof options.name !== 'string') {
397 err = new Error('The `name` option is required and must be a string');
398 err.statusCode = 400;
399
400 promise = Bluebird.reject(err);
401 }
402
403 if (options.code && typeof options.code !== 'string') {
404 err = new Error('The `code` option must be a string');
405 err.statusCode = 400;
406
407 promise = Bluebird.reject(err);
408 }
409
410 if (options.url && typeof options.url !== 'string') {
411 err = new Error('The `url` option must be a string');
412 err.statusCode = 400;
413
414 promise = Bluebird.reject(err);
415 }
416
417 if (options.code && options.url) {
418 err = new Error('Either the `code` or `url` option can be specified, but not both');
419 err.statusCode = 400;
420
421 promise = Bluebird.reject(err);
422 }
423
424 if (!err) {
425 var url = this.url + '/api/webtask/'
426 + (options.container || this.container) + '/' + options.name;
427 var payload = {};
428 if (options.secrets) payload.secrets = options.secrets;
429 if (options.code) payload.code = options.code;
430 if (options.url) payload.url = options.url;
431 if (options.meta) payload.meta = options.meta;
432 if (options.host) payload.host = options.host;
433 var request = Superagent
434 .put(url)
435 .set('Authorization', 'Bearer ' + this.token)
436 .send(payload)
437
438 var self = this;
439 promise = this.issueRequest(request)
440 .then(function (res) {
441 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 });
442 });
443 }
444
445 return cb ? promise.nodeify(cb) : promise;
446};
447
448
449/**
450 * Remove a named webtask from the webtask container
451 *
452 * @param {Object} options - Options
453 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
454 * @param {String} options.name - The name of the cron job.
455 * @param {Function} [cb] - Optional callback function for node-style callbacks.
456 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
457 */
458Sandbox.prototype.removeWebtask = function (options, cb) {
459 if (!options) options = {};
460
461 var promise;
462
463 if (!options.name) {
464 var err = new Error('Missing required option: `options.name`');
465 err.statusCode = 400;
466
467 promise = Bluebird.reject(err);
468 } else {
469 var url = this.url + '/api/webtask/'
470 + (options.container || this.container) + '/' + options.name;
471 var request = Superagent
472 .del(url)
473 .set('Authorization', 'Bearer ' + this.token);
474
475 promise = this.issueRequest(request)
476 .return(true);
477 }
478
479 return cb ? promise.nodeify(cb) : promise;
480};
481
482/**
483 * Update an existing webtask's code, secrets or other claims
484 *
485 * Note that this method should be used with caution as there is the potential
486 * for a race condition where another agent updates the webtask between the time
487 * that the webtask details and claims are resolved and when the webtask
488 * update is issued.
489 *
490 * @param {Object} options - Options
491 * @param {String} options.name - Name of the webtask to update
492 * @param {String} [options.code] - Updated code for the webtask
493 * @param {String} [options.url] - Updated code URL for the webtask
494 * @param {String} [options.secrets] - If `false`, remove existing secrets, if an object update secrets, otherwise preserve
495 * @param {String} [options.params] - If `false`, remove existing params, if an object update params, otherwise preserve
496 * @param {String} [options.host] - If `false`, remove existing host, if a string update host, otherwise preserve
497 * @param {Function} [cb] - Optional callback function for node-style callbacks.
498 * @return {Promise} A Promise that will be fulfilled with an instance of Webtask representing the updated webtask
499 */
500Sandbox.prototype.updateWebtask = function (options, cb) {
501 if (!options) options = {};
502
503 var self = this;
504 var err;
505 var promise;
506
507 if (typeof options.name !== 'string') {
508 err = new Error('The `name` option is required and must be a string');
509 err.statusCode = 400;
510
511 promise = Bluebird.reject(err);
512 }
513
514 if (options.code && typeof options.code !== 'string') {
515 err = new Error('The `code` option must be a string');
516 err.statusCode = 400;
517
518 promise = Bluebird.reject(err);
519 }
520
521 if (options.url && typeof options.url !== 'string') {
522 err = new Error('The `url` option must be a string');
523 err.statusCode = 400;
524
525 promise = Bluebird.reject(err);
526 }
527
528 if (options.code && options.url) {
529 err = new Error('Either the `code` or `url` option can be specified, but not both');
530 err.statusCode = 400;
531
532 promise = Bluebird.reject(err);
533 }
534
535 if (!err) {
536 promise = this.getWebtask({ name: options.name })
537 .then(onWebtask);
538
539 }
540
541 return cb ? promise.nodeify(cb) : promise;
542
543
544 function onWebtask(webtask) {
545 return webtask.inspect({ decrypt: options.secrets !== false, fetch_code: options.code !== false, meta: options.meta !== false })
546 .then(onInspection);
547
548
549 function onInspection(claims) {
550 var newClaims = {
551 ten: claims.ten,
552 jtn: claims.jtn,
553 };
554
555 ['nbf', 'exp', 'dd', 'dr', 'ls', 'lm', 'lh', 'ld', 'lw', 'lo', 'lts', 'ltm', 'lth', 'ltd', 'ltw', 'lto']
556 .forEach(function (claim) {
557 if (claims[claim]) {
558 newClaims[claim] = claims[claim];
559 }
560 });
561
562 if (options.host !== false && (options.host || claims.host)) {
563 newClaims.host = options.host || claims.host;
564 }
565 if (typeof options.parseBody !== 'undefined' || typeof options.parse !== 'undefined' || claims.pb) {
566 newClaims.pb = +(options.parseBody || options.parse || claims.pb);
567 }
568 if (typeof options.mergeBody !== 'undefined' || typeof options.merge !== 'undefined' || claims.mb) {
569 newClaims.mb = +(options.mergeBody || options.merge || claims.mb);
570 }
571 if (options.secrets !== false && (options.secrets || claims.ectx)) {
572 newClaims.ectx = options.secrets || claims.ectx;
573 }
574 if (options.params !== false && (options.params || claims.pctx)) {
575 newClaims.pctx = options.params || claims.pctx;
576 }
577 if (options.meta !== false && (options.meta || claims.meta)) {
578 newClaims.meta = options.meta || claims.meta;
579 }
580 if (options.url) {
581 newClaims.url = options.url;
582 }
583 if (options.code) {
584 newClaims.code = options.code;
585 }
586
587 return self.createRaw(newClaims, { include_webtask_url: options.include_webtask_url });
588 }
589 }
590};
591
592/**
593 * List named webtasks from the webtask container
594 *
595 * @param {Object} options - Options
596 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
597 * @param {Function} [cb] - Optional callback function for node-style callbacks.
598 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
599 */
600Sandbox.prototype.listWebtasks = function (options, cb) {
601 if (!options) options = {};
602
603 var url = this.url + '/api/webtask/'
604 + (options.container || this.container);
605 var request = Superagent
606 .get(url)
607 .set('Authorization', 'Bearer ' + this.token)
608 .accept('json');
609
610 if (options.offset) request.query({ offset: options.offset });
611 if (options.limit) request.query({ limit: options.limit });
612 if (options.meta) {
613 for (var m in options.meta) {
614 request.query({ meta: m + ':' + options.meta[m] });
615 }
616 }
617
618 var self = this;
619 var promise = this.issueRequest(request)
620 .get('body')
621 .map(function (webtask) {
622 return new Webtask(self, webtask.token, { name: webtask.name, meta: webtask.meta, webtask_url: webtask.webtask_url });
623 });
624
625 return cb ? promise.nodeify(cb) : promise;
626};
627
628/**
629 * Create a cron job from an already-existing webtask
630 *
631 * @param {Object} options - Options for creating a cron job
632 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
633 * @param {String} options.name - The name of the cron job.
634 * @param {String} [options.token] - The webtask token that will be used to run the job.
635 * @param {String} options.schedule - The cron schedule that will be used to determine when the job will be run.
636 * @param {String} options.tz - The cron timezone (IANA timezone).
637 * @param {String} options.meta - The cron metadata (set of string key value pairs).
638 * @param {Function} [cb] - Optional callback function for node-style callbacks.
639 * @returns {Promise} A Promise that will be fulfilled with a {@see CronJob} instance.
640 */
641Sandbox.prototype.createCronJob = function (options, cb) {
642 options = defaults(options, { container: this.container });
643
644 var payload = {
645 schedule: options.schedule,
646 };
647 if (options.token) payload.token = options.token;
648
649 if (options.state) {
650 payload.state = options.state;
651 }
652 if (options.meta) {
653 payload.meta = options.meta;
654 }
655 if (options.tz) {
656 payload.tz = options.tz;
657 }
658
659 var request = Superagent
660 .put(this.url + '/api/cron/' + options.container + '/' + options.name)
661 .set('Authorization', 'Bearer ' + this.token)
662 .send(payload)
663 .accept('json');
664
665 var promise = this.issueRequest(request)
666 .get('body')
667 .then(this._createCronJob.bind(this));
668
669 return cb ? promise.nodeify(cb) : promise;
670};
671
672/**
673 * Remove an existing cron job
674 *
675 * @param {Object} options - Options for removing the cron job
676 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
677 * @param {String} options.name - The name of the cron job.
678 * @param {Function} [cb] - Optional callback function for node-style callbacks.
679 * @returns {Promise} A Promise that will be fulfilled with the response from removing the job.
680 */
681Sandbox.prototype.removeCronJob = function (options, cb) {
682 options = defaults(options, { container: this.container });
683
684 var request = Superagent
685 .del(this.url + '/api/cron/' + options.container + '/' + options.name)
686 .set('Authorization', 'Bearer ' + this.token)
687 .accept('json');
688
689 var promise = this.issueRequest(request)
690 .get('body');
691
692 return cb ? promise.nodeify(cb) : promise;
693};
694
695/**
696 * Set an existing cron job's state
697 *
698 * @param {Object} options - Options for updating the cron job's state
699 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
700 * @param {String} options.name - The name of the cron job.
701 * @param {String} options.state - The new state of the cron job.
702 * @param {Function} [cb] - Optional callback function for node-style callbacks.
703 * @returns {Promise} A Promise that will be fulfilled with the response from removing the job.
704 */
705Sandbox.prototype.setCronJobState = function (options, cb) {
706 options = defaults(options, { container: this.container });
707
708 var request = Superagent
709 .put(this.url + '/api/cron/' + options.container + '/' + options.name + '/state')
710 .set('Authorization', 'Bearer ' + this.token)
711 .send({
712 state: options.state,
713 })
714 .accept('json');
715
716 var promise = this.issueRequest(request)
717 .get('body');
718
719 return cb ? promise.nodeify(cb) : promise;
720};
721
722/**
723 * List cron jobs associated with this profile
724 *
725 * @param {Object} [options] - Options for listing cron jobs.
726 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
727 * @param {Function} [cb] - Optional callback function for node-style callbacks.
728 * @returns {Promise} A Promise that will be fulfilled with an Array of {@see CronJob} instances.
729 */
730Sandbox.prototype.listCronJobs = function (options, cb) {
731 if (typeof options === 'function') {
732 cb = options;
733 options = null;
734 }
735
736 if (!options) options = {};
737
738 options = defaults(options, { container: this.container });
739
740 var request = Superagent
741 .get(this.url + '/api/cron/' + options.container)
742 .set('Authorization', 'Bearer ' + this.token)
743 .accept('json');
744
745 if (options.offset) request.query({ offset: options.offset });
746 if (options.limit) request.query({ limit: options.limit });
747 if (options.meta) {
748 for (var m in options.meta) {
749 request.query({ meta: m + ':' + options.meta[m] });
750 }
751 }
752
753 var promise = this.issueRequest(request)
754 .get('body')
755 .map(this._createCronJob.bind(this));
756
757 return cb ? promise.nodeify(cb) : promise;
758};
759
760/**
761 * Get a CronJob instance associated with an existing cron job
762 *
763 * @param {Object} options - Options for retrieving the cron job.
764 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
765 * @param {String} options.name - The name of the cron job.
766 * @param {Function} [cb] - Optional callback function for node-style callbacks.
767 * @returns {Promise} A Promise that will be fulfilled with a {@see CronJob} instance.
768 */
769Sandbox.prototype.getCronJob = function (options, cb) {
770 options = defaults(options, { container: this.container });
771
772 var request = Superagent
773 .get(this.url + '/api/cron/' + options.container + '/' + options.name)
774 .set('Authorization', 'Bearer ' + this.token)
775 .accept('json');
776
777 var promise = this.issueRequest(request)
778 .get('body')
779 .then(this._createCronJob.bind(this));
780
781 return cb ? promise.nodeify(cb) : promise;
782};
783
784
785/**
786 * Get the historical results of executions of an existing cron job.
787 *
788 * @param {Object} options - Options for retrieving the cron job.
789 * @param {String} [options.container] - The container in which the job will run. Defaults to the current profile's container.
790 * @param {String} options.name - The name of the cron job.
791 * @param {String} [options.offset] - The offset to use when paging through results.
792 * @param {String} [options.limit] - The limit to use when paging through results.
793 * @param {Function} [cb] - Optional callback function for node-style callbacks.
794 * @returns {Promise} A Promise that will be fulfilled with an Array of cron job results.
795 */
796Sandbox.prototype.getCronJobHistory = function (options, cb) {
797 options = defaults(options, { container: this.container });
798
799 var request = Superagent
800 .get(this.url + '/api/cron/' + options.container + '/' + options.name + '/history')
801 .set('Authorization', 'Bearer ' + this.token)
802 .accept('json');
803
804 if (options.offset) request.query({offset: options.offset});
805 if (options.limit) request.query({limit: options.limit});
806
807 var promise = this.issueRequest(request)
808 .get('body')
809 .map(function (result) {
810 var auth0HeaderRx = /^x-auth0/;
811
812 result.scheduled_at = new Date(result.scheduled_at);
813 result.started_at = new Date(result.started_at);
814 result.completed_at = new Date(result.completed_at);
815
816 for (var header in result.headers) {
817 if (auth0HeaderRx.test(header)) {
818 try {
819 result.headers[header] = JSON.parse(result.headers[header]);
820 } catch (__) {
821 // Do nothing
822 }
823 }
824 }
825
826 return result;
827 });
828
829 return cb ? promise.nodeify(cb) : promise;
830};
831
832/**
833 * Inspect an existing webtask token to resolve code and/or secrets
834 *
835 * @param {Object} options - Options for inspecting the webtask.
836 * @param {Boolean} options.token - The token that you would like to inspect.
837 * @param {Boolean} [options.decrypt] - Decrypt the webtask's secrets.
838 * @param {Boolean} [options.fetch_code] - Fetch the code associated with the webtask.
839 * @param {Function} [cb] - Optional callback function for node-style callbacks.
840 * @returns {Promise} A Promise that will be fulfilled with the resolved webtask data.
841 */
842Sandbox.prototype.inspectToken = function (options, cb) {
843 options = defaults(options, { container: this.container });
844
845 var request = Superagent
846 .get(this.url + '/api/tokens/inspect')
847 .query({ token: options.token })
848 .set('Authorization', 'Bearer ' + this.token)
849 .accept('json');
850
851 if (options.decrypt) request.query({ decrypt: options.decrypt });
852 if (options.fetch_code) request.query({ fetch_code: options.fetch_code });
853 if (options.meta) request.query({ meta: options.meta });
854
855 var promise = this.issueRequest(request)
856 .get('body');
857
858 return cb ? promise.nodeify(cb) : promise;
859};
860
861/**
862 * Inspect an existing named webtask to resolve code and/or secrets
863 *
864 * @param {Object} options - Options for inspecting the webtask.
865 * @param {Boolean} options.name - The named webtask that you would like to inspect.
866 * @param {Boolean} [options.decrypt] - Decrypt the webtask's secrets.
867 * @param {Boolean} [options.fetch_code] - Fetch the code associated with the webtask.
868 * @param {Function} [cb] - Optional callback function for node-style callbacks.
869 * @returns {Promise} A Promise that will be fulfilled with the resolved webtask data.
870 */
871Sandbox.prototype.inspectWebtask = function (options, cb) {
872 options = defaults(options, { container: this.container });
873
874 var promise = this.securityVersion === 'v2'
875 ? this.getWebtask({ name: options.name, decrypt: options.decrypt, fetch_code: options.fetch_code })
876 : this.getWebtask({ name: options.name })
877 .call('inspect', { decrypt: options.decrypt, fetch_code: options.fetch_code, meta: options.meta });
878
879 return cb ? promise.nodeify(cb) : promise;
880};
881
882
883/**
884 * Revoke a webtask token
885 *
886 * @param {String} token - The token that should be revoked
887 * @param {Function} [cb] - Optional callback function for node-style callbacks
888 * @returns {Promise} A Promise that will be fulfilled with the token
889 * @see https://webtask.io/docs/api_revoke
890 */
891Sandbox.prototype.revokeToken = function (token, cb) {
892 var request = Superagent
893 .post(this.url + '/api/tokens/revoke')
894 .set('Authorization', 'Bearer ' + this.token)
895 .query({ token: token });
896
897 var promise = this.issueRequest(request);
898
899 return cb ? promise.nodeify(cb) : promise;
900};
901
902/**
903 * List versions of a given node module that are available on the platform
904 *
905 * @param {Object} options - Options
906 * @param {String} options.name - Name of the node module
907 * @param {Function} [cb] - Optional callback function for node-style callbacks
908 * @returns {Promise} A Promise that will be fulfilled with the token
909 */
910Sandbox.prototype.listNodeModuleVersions = function (options, cb) {
911 var request = Superagent
912 .get(this.url + `/api/env/node/modules/${encodeURIComponent(options.name)}`);
913
914 var promise = this.issueRequest(request)
915 .get('body');
916
917 return cb ? promise.nodeify(cb) : promise;
918};
919
920/**
921 * Ensure that a set of modules are available on the platform
922 *
923 * @param {Object} options - Options
924 * @param {Array} options.modules - Array of { name, version } pairs
925 * @param {Boolean} options.reset - Trigger a rebuild of the modules (Requires administrative token)
926 * @param {Function} [cb] - Optional callback function for node-style callbacks
927 * @returns {Promise} A Promise that will be fulfilled with an array of { name, version, state } objects
928*/
929Sandbox.prototype.ensureNodeModules = function (options, cb) {
930 var request = Superagent
931 .post(this.url + '/api/env/node/modules')
932 .send({ modules: options.modules })
933 .set('Authorization', 'Bearer ' + this.token);
934
935 if (options.reset) {
936 request.query({ reset: 1 });
937 }
938
939 var promise = this.issueRequest(request)
940 .get('body');
941
942 return cb ? promise.nodeify(cb) : promise;
943};
944
945/**
946 * Update the storage associated to the a webtask
947 *
948 * @param {Object} options - Options
949 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
950 * @param {String} options.name - The name of the webtask.
951 * @param {Object} storage - storage
952 * @param {Object} storage.data - The data to be stored
953 * @param {String} storage.etag - Pass in an optional string to be used for optimistic concurrency control to prevent simultaneous updates of the same data.
954 * @param {Function} [cb] - Optional callback function for node-style callbacks.
955 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
956 */
957Sandbox.prototype.updateStorage = function (storage, options, cb) {
958 var promise;
959
960 if (!options || !options.name) {
961 var err = new Error('Missing required option: `options.name`');
962 err.statusCode = 400;
963
964 promise = Bluebird.reject(err);
965 }
966 else {
967 var obj = {
968 data: JSON.stringify(storage.data)
969 }
970
971 if (storage.etag) {
972 obj.etag = storage.etag.toString();
973 }
974
975 var request = Superagent
976 .put(this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name + '/data')
977 .send(obj)
978 .set('Authorization', 'Bearer ' + this.token)
979 .accept('json');
980
981 promise = this.issueRequest(request)
982 .get('body');
983 }
984
985 return cb ? promise.nodeify(cb) : promise;
986};
987
988/**
989 * Read the storage associated to the a webtask
990 *
991 * @param {Object} options - Options
992 * @param {String} [options.container] - Set the webtask container. Defaults to the profile's container.
993 * @param {String} options.name - The name of the webtask.
994 * @param {Function} [cb] - Optional callback function for node-style callbacks.
995 * @return {Promise} A Promise that will be fulfilled with an array of Webtasks
996 */
997Sandbox.prototype.getStorage = function (options, cb) {
998 var promise;
999
1000 if (!options || !options.name) {
1001 var err = new Error('Missing required option: `options.name`');
1002 err.statusCode = 400;
1003
1004 promise = Bluebird.reject(err);
1005 }
1006 else {
1007 var request = Superagent
1008 .get(this.url + '/api/webtask/' + (options.container || this.container) + '/' + options.name + '/data')
1009 .set('Authorization', 'Bearer ' + this.token)
1010 .accept('json');
1011
1012 promise = this.issueRequest(request)
1013 .get('body')
1014 .then(function (result) {
1015 var storage = result;
1016
1017 try {
1018 storage.data = JSON.parse(result.data);
1019 } catch(e) {
1020 // TODO: Log somewhere
1021 }
1022
1023 return storage;
1024 });
1025 }
1026
1027 return cb ? promise.nodeify(cb) : promise;
1028}
1029
1030Sandbox.limits = {
1031 container: {
1032 second: 'ls',
1033 minute: 'lm',
1034 hour: 'lh',
1035 day: 'ld',
1036 week: 'lw',
1037 month: 'lo'
1038 },
1039 token: {
1040 second: 'lts',
1041 minute: 'ltm',
1042 hour: 'lth',
1043 day: 'ltd',
1044 week: 'ltw',
1045 month: 'lto',
1046 },
1047};
1048
1049/**
1050 * Create a Sandbox instance from a webtask token
1051 *
1052 * @param {String} token - The webtask token from which the Sandbox profile will be derived.
1053 * @param {Object} options - The options for creating the Sandbox instance that override the derived values from the token.
1054 * @param {String} [options.url] - The url of the webtask cluster. Defaults to the public 'webtask.it.auth0.com' cluster.
1055 * @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.
1056 * @param {String} options.token - The Webtask Token. See: https://webtask.io/docs/api_issue.
1057 * @returns {Sandbox} A {@see Sandbox} instance whose url, token and container were derived from the given webtask token.
1058 *
1059 * @alias module:sandboxjs.fromToken
1060 */
1061Sandbox.fromToken = function (token, options) {
1062 var config = defaults({}, options, Sandbox.optionsFromJwt(token));
1063
1064 return Sandbox.init(config);
1065};
1066
1067/**
1068 * Create a Sandbox instance
1069 *
1070 * @param {Object} options - The options for creating the Sandbox instance.
1071 * @param {String} [options.url] - The url of the webtask cluster. Defaults to the public 'webtask.it.auth0.com' cluster.
1072 * @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.
1073 * @param {String} options.token - The Webtask Token. See: https://webtask.io/docs/api_issue.
1074 * @returns {Sandbox} A {@see Sandbox} instance.
1075 *
1076 * @alias module:sandboxjs.init
1077 */
1078Sandbox.init = function (options) {
1079 if (typeof options !== 'object') throw new Error('Expecting an options Object, got `' + typeof options + '`.');
1080 if (!options.container) throw new Error('A Sandbox instance cannot be created without a container.');
1081 if (typeof options.container !== 'string') throw new Error('Only String containers are supported, got `' + typeof options.container + '`.');
1082 if (typeof options.token !== 'string') throw new Error('A Sandbox instance cannot be created without a token.');
1083
1084 defaults(options, {
1085 url: 'https://webtask.it.auth0.com',
1086 });
1087
1088 return new Sandbox(options);
1089};
1090
1091Sandbox.optionsFromJwt = function (jwt) {
1092 var claims = Decode(jwt);
1093
1094 if (!claims) throw new Error('Unable to decode token `' + jwt + '` (https://jwt.io/#id_token=' + jwt + ').');
1095
1096 // What does the
1097 var ten = claims.ten;
1098
1099 if (!ten) throw new Error('Invalid token, missing `ten` claim `' + jwt + '` (https://jwt.io/#id_token=' + jwt + ').');
1100
1101 if (Array.isArray(ten)) {
1102 ten = ten[0];
1103 } else {
1104 // Check if the `ten` claim is a RegExp
1105 var matches = ten.match(/\/(.+)\//);
1106 if (matches) {
1107 try {
1108 var regex = new RegExp(matches[1]);
1109 var gen = new RandExp(regex);
1110
1111 // Monkey-patch RandExp to be deterministic
1112 gen.randInt = function (l) { return l; };
1113
1114 ten = gen.gen();
1115 } catch (err) {
1116 throw new Error('Unable to derive containtainer name from `ten` claim `' + claims.ten + '`: ' + err.message + '.');
1117 }
1118 }
1119 }
1120
1121 if (typeof ten !== 'string' || !ten) throw new Error('Expecting `ten` claim to be a non-blank string, got `' + typeof ten + '`, with value `' + ten + '`.');
1122
1123 return {
1124 container: ten,
1125 token: jwt,
1126 };
1127};