├── .eslintrc ├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin ├── _plugins │ ├── profile.js │ └── profileOptions.js ├── auth0_create.js ├── auth0_ls.js ├── auth0_scaffold.js ├── auth0_toggle.js ├── browse.js ├── browser_logs.js ├── create.js ├── cron.js ├── cron │ ├── create.js │ ├── get.js │ ├── history.js │ ├── ls.js │ ├── reschedule.js │ ├── rm.js │ ├── schedule.js │ └── update.js ├── debug.js ├── edit.js ├── init.js ├── inspect.js ├── logs.js ├── ls.js ├── modules.js ├── modules │ ├── add.js │ └── versions.js ├── profile.js ├── profile │ ├── get.js │ ├── init.js │ ├── ls.js │ ├── nuke.js │ └── rm.js ├── rm.js ├── serve.js ├── serveCommon.js ├── token.js ├── token │ ├── create.js │ ├── inspect.js │ └── revoke.js ├── update.js └── wt ├── index.js ├── lib ├── config.js ├── createWebtask.js ├── cron.js ├── errors.js ├── keyValList2Object.js ├── logs.js ├── modules.js ├── printCronJob.js ├── printProfile.js ├── printTokenDetails.js ├── printWebtask.js ├── printWebtaskDetails.js ├── userAuthenticator.js ├── userVerifier.js ├── util.js ├── validateCreateArgs.js └── webtaskCreator.js ├── opslevel.yml ├── package-lock.json ├── package.json ├── sample-webtasks ├── README.md ├── attack-cpu.js ├── attack-forkbomb.js ├── attack-ram.js ├── bundled-webtask │ ├── .eslintrc │ ├── data.js │ ├── index.js │ └── package.json ├── counter.js ├── csharp.js ├── es6-template-literals.js ├── es6.js ├── express.js ├── github-tag-hook.js ├── google-places-api.js ├── hello-world.js ├── html-response.js ├── httppost.js ├── json-response.js ├── logging.js ├── mongodb.js ├── request.js ├── unverified-emails.js └── url-query-parameter.js └── samples └── pkce.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended" 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | pull_request_target: {} 5 | push: 6 | branches: ['master', 'main'] 7 | jobs: 8 | semgrep: 9 | name: Scan 10 | runs-on: ubuntu-latest 11 | container: 12 | image: returntocorp/semgrep 13 | if: (github.actor != 'dependabot[bot]' && github.actor != 'snyk-bot') 14 | steps: 15 | - uses: actions/checkout@v3 16 | - run: semgrep ci 17 | env: 18 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | .vscode 30 | reproductions 31 | scaffold 32 | test 33 | .secrets 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Auth0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Deprecated ⚠️ 2 | 3 | **This repository is deprecated and is no longer receiving support or maintenance** 4 | 5 | # Webtask CLI: all you need is code 6 | 7 | Command line tool for using [webtasks](https://webtask.io) to create microservices in seconds. 8 | 9 | ## Setup 10 | 11 | ```bash 12 | $ npm i -g wt-cli 13 | $ wt init 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### Create a webtask 19 | 20 | Write webtask code to the *hello-world.js* file: 21 | 22 | ```javascript 23 | module.exports = function (cb) { 24 | cb(null, 'Hello webtasks!'); 25 | } 26 | ``` 27 | 28 | ```bash 29 | wt create hello-world.js 30 | ``` 31 | 32 | and call it... 33 | 34 | ```bash 35 | curl https://webtask.it.auth0.com/api/run/{yours}/hello-world 36 | ``` 37 | 38 | ### Create a webtask (from a public URL) 39 | 40 | ```bash 41 | wt create https://raw.githubusercontent.com/auth0/wt-cli/master/sample-webtasks/html-response.js \ 42 | --name html-response-url 43 | ``` 44 | 45 | ### Create a webtask with a secret 46 | 47 | ```bash 48 | wt create https://raw.githubusercontent.com/auth0/wt-cli/master/sample-webtasks/mongodb.js \ 49 | --name mongo \ 50 | --secret MONGO_URL=mongodb://webtask:supersecret@ds047592.mongolab.com:47592/webtask-examples 51 | ``` 52 | 53 | > This is a real mongodb URL (powered by mongolab), no guarantee that it will work :) 54 | 55 | ### Create a webtask that integrates with express.js 56 | 57 | ```bash 58 | wt create https://raw.githubusercontent.com/auth0/wt-cli/master/sample-webtasks/express.js \ 59 | --name express \ 60 | --dependency express \ 61 | --dependency body-parser 62 | ``` 63 | 64 | 65 | ### Log streaming 66 | 67 | ```bash 68 | wt logs 69 | ``` 70 | 71 | ### Cron a webtask (long running) 72 | 73 | ```bash 74 | wt cron schedule -n mongocron \ 75 | -s MONGO_URL=mongodb://webtask:supersecret@ds047592.mongolab.com:47592/webtask-examples \ 76 | "*/10 * * * *" \ 77 | https://raw.githubusercontent.com/auth0/wt-cli/master/sample-webtasks/mongodb.js 78 | ``` 79 | 80 | > This cron will insert a document in a mongo collection every 10 minutes 81 | 82 | ### Get cron history 83 | 84 | ```bash 85 | wt cron history mongogron 86 | ``` 87 | 88 | ### Get all scheduled webtasks 89 | 90 | ```bash 91 | wt cron ls 92 | ``` 93 | 94 | # Auth0 CLI: extend Auth0 with custom code (BETA) 95 | 96 | Auth0 hooks enable you to extend the Auth0 platform with custom code. Auth0 CLI allows you to manage Auth0 hooks for your Auth0 account. 97 | 98 | Auth0 hooks are webtasks associated with specific extensibility points of the Auth0 platform that are invoked by Auth0 runtime to execute your custom logic. 99 | 100 | ## Setup 101 | 102 | Follow the instructions from the Account > Webtasks panel on the Auth0 management dashboard to install and configure `wt-cli`. The package now includes `auth0` binary in addition to `wt`. 103 | 104 | **NOTE** While Auth0 CLI is in BETA, the `wt-cli` tool must be installed from a branch of this repository instead of the public npm registry. Use `npm i -g auth0/wt-cli#auth0` to install the tool. The rest of the instructions on the Auth0 management portal applies without changes. 105 | 106 | ## Overview 107 | 108 | Auth0 CLI allows you to create, list, enable/disable, edit, and remove Auth0 hooks associated with specific extensibility points within the Auth0 platform, as well as receive real-time logging information generated by custom code. 109 | 110 | The list of supported extensibility points will be growing over time. Currently, the following extensibility points are supported: 111 | 112 | [client-credentials-exchange](https://github.com/auth0/auth0-ext-compilers/blob/master/client-credentials-exchange.md) 113 | [password-exchange](https://github.com/auth0/auth0-ext-compilers/blob/master/password-exchange.md) 114 | [pre-user-registration](https://github.com/auth0/auth0-ext-compilers/blob/master/pre-user-registration.md) 115 | [post-user-registration](https://github.com/auth0/auth0-ext-compilers/blob/master/post-user-registration.md) 116 | 117 | For each of the extensibility points, there can be several Auth0 hooks created. A hook can be enabled or disabled, but only up to one hook per extensibility point can be enabled at a time. Disabled hooks are useful for staging new functionality. 118 | 119 | ## Synopsis 120 | 121 | The script below assumes you are creating an Auth0 hook for the `pre-user-registration` extensibility point using profile name `tj-default`. You can use any of the extensibility points listed above and the actual profile name has been provided to you during `wt-cli` setup. 122 | 123 | ```bash 124 | # Scaffold sample code of an Auth0 hook: 125 | auth0 scaffold -t pre-user-registration > file.js 126 | 127 | # Create a new, disabled Auth0 hook: 128 | auth0 create -t pre-user-registration --name my-extension-1 -p tj-default file.js 129 | 130 | # Edit code of the Auth0 hook: 131 | auth0 edit my-extension-1 132 | 133 | # Enable the newly created extensibility point (all other hooks associated 134 | # with the same extensibility point will be disabled): 135 | auth0 enable my-extension-1 -p tj-default 136 | 137 | # List hooks for a specific extensibility point: 138 | auth0 ls -t pre-user-registration -p tj-default 139 | 140 | # List all Auth0 hooks on your account: 141 | auth0 ls -p tj-default 142 | 143 | # Access streaming, real-time logs of all of your hooks: 144 | auth0 logs -p tj-default 145 | 146 | # Disable a hook: 147 | auth0 disable my-extension-1 -p tj-default 148 | 149 | # Delete a hook: 150 | auth0 rm my-extension-1 -p tj-default 151 | ``` 152 | 153 | # Closing remarks 154 | 155 | ## Working behind a proxy 156 | 157 | `wt-cli` supports operating behind a proxy as of `v6.1.0`. The cli relies on the `HTTP_PROXY` (or `http_proxy`) environment variable to determine if proxy support needs to be enabled. The `HTTP_PROXY` environment variable must be set to a uri according to the following table: 158 | 159 | | Protocol | Proxy Agent for `http` requests | Proxy Agent for `https` requests | Example 160 | |:----------:|:-------------------------------:|:--------------------------------:|:--------: 161 | | `http` | [http-proxy-agent](https://github.com/TooTallNate/node-http-proxy-agent) | [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) | `http://proxy-server-over-tcp.com:3128` 162 | | `https` | [http-proxy-agent](https://github.com/TooTallNate/node-http-proxy-agent) | [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) | `https://proxy-server-over-tls.com:3129` 163 | | `socks(v5)`| [socks-proxy-agent](https://github.com/TooTallNate/node-socks-proxy-agent) | [socks-proxy-agent](https://github.com/TooTallNate/node-socks-proxy-agent) | `socks://username:password@some-socks-proxy.com:9050` (username & password are optional) 164 | | `socks5` | [socks-proxy-agent](https://github.com/TooTallNate/node-socks-proxy-agent) | [socks-proxy-agent](https://github.com/TooTallNate/node-socks-proxy-agent) | `socks5://username:password@some-socks-proxy.com:9050` (username & password are optional) 165 | | `socks4` | [socks-proxy-agent](https://github.com/TooTallNate/node-socks-proxy-agent) | [socks-proxy-agent](https://github.com/TooTallNate/node-socks-proxy-agent) | `socks4://some-socks-proxy.com:9050` 166 | | `pac` | [pac-proxy-agent](https://github.com/TooTallNate/node-pac-proxy-agent) | [pac-proxy-agent](https://github.com/TooTallNate/node-pac-proxy-agent) | `pac+http://www.example.com/proxy.pac` 167 | 168 | > * See [http-proxy-agent](https://github.com/TooTallNate/node-proxy-agent/blob/master/README.md) for the source of this table. 169 | 170 | ## Issue Reporting 171 | 172 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 173 | 174 | ## Author 175 | 176 | [Auth0](https://auth0.com) 177 | 178 | ## License 179 | 180 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. 181 | -------------------------------------------------------------------------------- /bin/_plugins/profile.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Chalk = require('chalk'); 3 | var Cli = require('structured-cli'); 4 | var ConfigFile = require('../../lib/config'); 5 | var Sandbox = require('sandboxjs'); 6 | var SuperagentProxy = require('superagent-proxy'); 7 | var UserAuthenticator = require('../../lib/userAuthenticator'); 8 | var _ = require('lodash'); 9 | 10 | 11 | module.exports = { 12 | onBeforeConfigure: require('./profileOptions').onBeforeConfigure, 13 | onBeforeHandler: onBeforeHandler, 14 | }; 15 | 16 | function onBeforeHandler(context) { 17 | var args = context.args; 18 | 19 | return sandboxFromArguments(args) 20 | .then(onProfile); 21 | 22 | 23 | function onProfile(profile) { 24 | args.profile = profile; 25 | 26 | // Ensure V2 access token is fresh enough 27 | 28 | if (!args.profile.openid) return; // V1 webtask token, nothing to do 29 | 30 | // If V2 access token expires in less than 5 mins, get a new one 31 | 32 | var validUntil = new Date(args.profile.openid.valid_until); 33 | var now = Date.now(); 34 | if ((validUntil - now) < 5 * 60 * 1000) { 35 | var userAuthenticator = new UserAuthenticator({ 36 | sandboxUrl: args.profile.url, 37 | authorizationServer: args.profile.openid.authorization_server, 38 | audience: args.profile.openid.audience, 39 | clientId: args.profile.openid.client_id, 40 | refreshToken: args.profile.openid.refresh_token, 41 | }); 42 | 43 | return userAuthenticator 44 | .login({ 45 | container: args.profile.container, 46 | admin: args.profile.openid.scopes.indexOf('wt:admin') > -1, 47 | auth0: args.profile.openid.auth0, 48 | profileName: args.profile.name, 49 | requestedScopes: args.profile.openid.scope, 50 | }) 51 | .then(function (profile) { 52 | args.profile = profile; 53 | var config = new ConfigFile(); 54 | config.load(); 55 | return config.setProfile(profile.name, { 56 | url: profile.url, 57 | token: profile.token, 58 | container: profile.container, 59 | openid: profile.openid, 60 | }) 61 | .tap(function () { 62 | return config.save(); 63 | }); 64 | }); 65 | } 66 | else { 67 | return; // access token still valid more than 5 mins 68 | } 69 | } 70 | } 71 | 72 | function sandboxFromArguments(args, options) { 73 | return new Bluebird(function (resolve, reject) { 74 | if (!options) options = {}; 75 | 76 | if (args.token) { 77 | if (args.profile && !options.allowProfile) return reject(new Cli.error.invalid('--profile should not be specified with custom tokens')); 78 | if (args.container && args.url) { 79 | try { 80 | return resolve(Sandbox.init({ 81 | onBeforeRequest: [ 82 | onBeforeRequestProxy, 83 | onBeforeRequestRuntime 84 | ], 85 | container: args.container, 86 | token: args.token, 87 | url: args.url, 88 | })); 89 | } catch (e) { 90 | return reject(e); 91 | } 92 | } 93 | } 94 | 95 | var config = new ConfigFile(); 96 | var profile$ = config.load() 97 | .then(loadProfile) 98 | .then(onProfileLoaded); 99 | 100 | return resolve(profile$); 101 | 102 | function loadProfile(profiles) { 103 | if (_.isEmpty(profiles)) { 104 | throw Cli.error.hint('No webtask profiles found. To get started:\n' 105 | + Chalk.bold('$ wt init')); 106 | } 107 | 108 | return config.getProfile(args.profile); 109 | } 110 | 111 | function onBeforeRequestProxy(request) { 112 | const proxy = process.env.http_proxy || process.env.HTTP_PROXY; 113 | const result = proxy 114 | ? SuperagentProxy(request, proxy) 115 | : request; 116 | 117 | return result; 118 | } 119 | 120 | function onBeforeRequestRuntime(request) { 121 | if (args.runtime) { 122 | return request.set('x-wt-runtime', args.runtime); 123 | } 124 | 125 | return request; 126 | } 127 | 128 | function onProfileLoaded(profile) { 129 | if (args.container) profile.container = args.container; 130 | if (args.url) profile.url = args.url; 131 | if (args.token) profile.token = args.token; 132 | 133 | if (args.runtime) { 134 | profile.onBeforeRequest = Array.isArray(profile.onBeforeRequest) 135 | ? profile.onBeforeRequest.concat(onBeforeRequestRuntime) 136 | : [onBeforeRequestRuntime]; 137 | } 138 | 139 | return profile; 140 | } 141 | }); 142 | } 143 | -------------------------------------------------------------------------------- /bin/_plugins/profileOptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | 6 | module.exports = { 7 | onBeforeConfigure, 8 | }; 9 | 10 | 11 | function onBeforeConfigure(context) { 12 | const node = context.node; 13 | const options = { 14 | profile: { 15 | alias: 'p', 16 | description: 'Default to the webtask container, token, and URL from a stored profile.', 17 | type: 'string', 18 | }, 19 | container: { 20 | alias: 'c', 21 | description: 'Set the webtask container. This can be combined with --profile if you want to override the container or can be used with --url and --token to specify the webtask profile inline.', 22 | type: 'string', 23 | }, 24 | url: { 25 | description: 'Set the webtask server url. Defaults to https://sandbox.auth0-extend.com', 26 | type: 'string', 27 | }, 28 | token: { 29 | description: 'Set your authorizing webtask token. If you do not have a webtask token, one can be provisioned using `wt init`.', 30 | type: 'string', 31 | }, 32 | runtime: { 33 | description: 'Runtime version used during execution and persisting of webtasks. Example: node8|node12', 34 | type: 'string' 35 | }, 36 | }; 37 | 38 | node.addOptionGroup('Webtask profile', _.omit(options, _.get(context.node.config, 'profileOptions.hide', []))); 39 | } 40 | -------------------------------------------------------------------------------- /bin/auth0_create.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var CreateWebtask = require('../lib/createWebtask'); 4 | var ValidateCreateArgs = require('../lib/validateCreateArgs'); 5 | var Crypto = require('crypto'); 6 | var auth0Extensions = require('auth0-hooks-templates'); 7 | var extensionTypes = Object.keys(auth0Extensions).sort(); 8 | 9 | module.exports = Cli.createCommand('create', { 10 | description: 'Create or update Auth0 Hook', 11 | plugins: [ 12 | require('./_plugins/profile'), 13 | ], 14 | optionGroups: { 15 | 'Hook creation': { 16 | 'type': { 17 | alias: 't', 18 | description: 'Hook type, required. One of: ' + extensionTypes.join(', ') + '.', 19 | choices: extensionTypes, 20 | required: true, 21 | dest: 'extensionName', 22 | metavar: 'TYPE', 23 | type: 'string' 24 | }, 25 | 'secret': { 26 | action: 'append', 27 | alias: 's', 28 | defaultValue: [], 29 | description: 'Secret(s) exposed to your code as `secrets` on the webtask context object. These secrets will be encrypted and stored in a webtask token in such a way that only the webtask server is able to decrypt the secrets so that they may be exposed to your running webtask code.', 30 | dest: 'secrets', 31 | metavar: 'KEY=VALUE', 32 | type: 'string', 33 | }, 34 | 'secrets-file': { 35 | description: 'A file containing one secret per line in KEY=VALUE format', 36 | dest: 'secretsFile', 37 | metavar: 'FILENAME', 38 | type: 'string', 39 | }, 40 | 'meta': { 41 | action: 'append', 42 | defaultValue: [], 43 | description: 'Metadata describing the webtask. This is a set of string key value pairs.', 44 | dest: 'meta', 45 | metavar: 'KEY=VALUE', 46 | type: 'string', 47 | }, 48 | 'name': { 49 | alias: 'n', 50 | description: 'Name of the webtask. When specified, the resulting webtask token can be run at a special named webtask url and additional path segments are allowed (/api/run/{container}/{name}/*). This is important when using `webtask-tools` to expose an Express server as a webtask.', 51 | type: 'string' 52 | }, 53 | 'parse-body': { 54 | description: 'Automatically parse JSON and application/x-www-form-urlencoded request bodies. Use this with (ctx, req, res) webtask signatures if you want webtask runtime to parse the request body and store it in ctx.body.', 55 | type: 'boolean', 56 | dest: 'parseBody' 57 | }, 58 | 'bundle': { 59 | alias: 'b', 60 | description: 'Use `webtask-bundle` to bundle your code into a single file. This tool can compile ES2015 (ES6) code via Babel as well as packaging up a webtask composed of multiple files into a single file. The tool will scan your package.json for dependencies and will automatically bundle those that are not available on the webtask platform. Enabling --bundle-loose will prevent this check from doing strict semver range comparisons on dependencies.', 61 | type: 'boolean', 62 | }, 63 | 'bundle-minify': { 64 | description: 'Generate a minified production build', 65 | type: 'boolean', 66 | dest: 'minify' 67 | }, 68 | 'bundle-strict': { 69 | description: 'Enforce strict semver matching for bundling with `webtask-bundle`', 70 | dest: 'loose', 71 | action: 'storeFalse', 72 | defaultValue: true, 73 | type: 'boolean', 74 | }, 75 | 'capture': { 76 | description: 'Download and use the current code indicated by `url`. When you are developing a webtask whose code is remotely hosted, this option will automatically download the remote code before creating the webtask. This means that the webtask will continue to run even if the remote url becomes unavailable.', 77 | type: 'boolean', 78 | }, 79 | }, 80 | }, 81 | params: { 82 | 'file_or_url': { 83 | description: 'Path or URL of the extension\'s code. When not specified, code will be read from STDIN.', 84 | type: 'string', 85 | }, 86 | }, 87 | handler: handleCreate, 88 | }); 89 | 90 | // Command handler 91 | 92 | function handleCreate(args) { 93 | args = ValidateCreateArgs(args); 94 | var profile = args.profile; 95 | 96 | return profile.inspectWebtask({ 97 | name: args.name, 98 | meta: true 99 | }) 100 | .then(function (claims) { 101 | if (!claims.meta || claims.meta['auth0-extension'] !== 'runtime') 102 | throw Cli.error.invalid('Webtask ' + args.name + ' exists but is not an Auth0 hook. Please specify a different name using --name parameter.'); 103 | if (claims.meta['auth0-extension-name'] !== args.extensionName) 104 | throw Cli.error.invalid('Auth0 hook ' + args.name + ' exists but is of type ' + claims.meta['auth0-extension-name'] + '. Please specify a different name using --name parameter to avoid a conflict.'); 105 | return _create(claims); 106 | }, function (e) { 107 | if (e && e.statusCode !== 404) { 108 | throw e; 109 | } 110 | return _create(); 111 | }); 112 | 113 | function _create(claims) { 114 | args.params = []; 115 | args.meta['auth0-extension'] = 'runtime'; 116 | args.meta['auth0-extension-name'] = args.extensionName; 117 | args.meta['auth0-extension-disabled'] = claims ? claims.meta['auth0-extension-disabled'] : '1'; 118 | 119 | // If wt-compiler specified explicitly, use it. Otherwise use the default per hook type. 120 | if (!args.meta['wt-compiler']) 121 | args.meta['wt-compiler'] = auth0Extensions[args.extensionName].wtCompiler; 122 | 123 | // If updating existing hook, preserve the authentication secret 124 | var authSecret = claims && claims.meta['auth0-extension-secret'] || Crypto.randomBytes(32).toString('hex'); 125 | args.meta['auth0-extension-secret'] = authSecret; 126 | args.secrets['auth0-extension-secret'] = authSecret; 127 | 128 | return CreateWebtask(args, { 129 | action: 'created', 130 | onOutput: function (log, build, url) { 131 | var action = claims ? 'updated' : 'created'; 132 | var state = args.meta['auth0-extension-disabled'] === "1" ? 'disabled' : 'enabled'; 133 | log(Chalk.green('Auth0 hook ' + action + ' in ' + state + ' state.') + 134 | (state == 'disabled' ? (' To enable this hook to run in production, call:\n\n' 135 | + Chalk.green('$ auth0 enable ' + args.name)) : '')); 136 | } 137 | }); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /bin/auth0_ls.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var _ = require('lodash'); 4 | var keyValList2Object = require('../lib/keyValList2Object'); 5 | var auth0Extensions = require('auth0-hooks-templates'); 6 | var extensionTypes = Object.keys(auth0Extensions).sort(); 7 | 8 | 9 | module.exports = Cli.createCommand('ls', { 10 | description: 'List Auth0 hooks', 11 | plugins: [ 12 | require('./_plugins/profile'), 13 | ], 14 | handler: handleListAuth0Extensions, 15 | optionGroups: { 16 | 'Hook selection': { 17 | 'type': { 18 | alias: 't', 19 | description: 'Hook type. One of: ' + extensionTypes.join(', ') + '.', 20 | choices: extensionTypes, 21 | dest: 'extensionName', 22 | metavar: 'TYPE', 23 | type: 'string' 24 | }, 25 | }, 26 | 'Pagination': { 27 | 'offset': { 28 | type: 'int', 29 | description: 'Skip this many hooks', 30 | defaultValue: 0, 31 | }, 32 | 'limit': { 33 | type: 'int', 34 | description: 'Limit the results to this many hooks', 35 | defaultValue: 10, 36 | }, 37 | }, 38 | 'Output options': { 39 | 'output': { 40 | alias: 'o', 41 | description: 'Set the output format', 42 | choices: ['json'], 43 | type: 'string', 44 | } 45 | }, 46 | }, 47 | }); 48 | 49 | 50 | // Command handler 51 | 52 | function handleListAuth0Extensions(args) { 53 | var profile = args.profile; 54 | 55 | keyValList2Object(args, 'meta'); 56 | 57 | var meta = { 58 | 'auth0-extension': 'runtime' 59 | }; 60 | if (args.extensionName) 61 | meta['auth0-extension-name'] = args.extensionName; 62 | 63 | return profile.listWebtasks({ 64 | offset: args.offset, 65 | limit: args.limit, 66 | meta: meta 67 | }) 68 | .catch(function (err) { 69 | if (err.statusCode >= 500) throw Cli.error.serverError(err.message); 70 | if (err.statusCode >= 400) throw Cli.error.badRequest(err.message); 71 | 72 | throw err; 73 | }) 74 | .then(onWebtasks); 75 | 76 | 77 | function onWebtasks(webtasks) { 78 | var types = {}; 79 | var count = 0; 80 | _.forEach(webtasks, function (webtask) { 81 | count++; 82 | var json = webtask.toJSON(); 83 | var record = { 84 | name: json.name, 85 | type: args.extensionName || (webtask.meta && webtask.meta['auth0-extension-name']) || 'N/A', 86 | enabled: !!(webtask.meta && webtask.meta['auth0-extension-disabled'] !== "1") 87 | }; 88 | if (!types[record.type]) { 89 | types[record.type] = [ record ]; 90 | } 91 | else { 92 | types[record.type].push(record); 93 | } 94 | }); 95 | 96 | if (args.output === 'json') { 97 | console.log(JSON.stringify(types, null, 2)); 98 | } else { 99 | var typeNames = Object.keys(types).sort(); 100 | typeNames.forEach(function (type) { 101 | console.log(Chalk.blue(type)); 102 | printExtensions(types[type]); 103 | console.log(); 104 | }); 105 | 106 | function printExtensions(extensions) { 107 | extensions.sort(function (a,b) { return a.name > b.name; }); 108 | extensions.forEach(function (e) { 109 | console.log(' ' + e.name + Chalk.green(e.enabled ? ' (enabled)' : '')); 110 | }) 111 | } 112 | 113 | if (!typeNames.length) { 114 | if (args.offset) { 115 | console.log('You have fewer than %s hooks.', Chalk.bold(args.offset)); 116 | } else { 117 | console.log(Chalk.green('You do not have any hooks. To get started, try:\n\n' 118 | + Chalk.bold('$ auth0 ' + (args.extensionName || '{extension_type}') + ' scaffold\n'))); 119 | } 120 | } else if (count === args.limit) { 121 | console.log(Chalk.green('Successfully listed hooks %s to %s. To list more try:'), Chalk.bold(args.offset + 1), Chalk.bold(args.offset + count)); 122 | console.log(Chalk.bold('$ auth0 ' + (args.extensionName ? args.extensionName + ' ' : '') + 'ls --offset %d'), args.offset + args.limit); 123 | } else { 124 | console.log(Chalk.green('Successfully listed hooks %s to %s.'), Chalk.bold(args.offset + 1), Chalk.bold(args.offset + count)); 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /bin/auth0_scaffold.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var _ = require('lodash'); 4 | var keyValList2Object = require('../lib/keyValList2Object'); 5 | var auth0Extensions = require('auth0-hooks-templates'); 6 | var extensionTypes = Object.keys(auth0Extensions).sort(); 7 | 8 | 9 | module.exports = Cli.createCommand('scaffold', { 10 | description: 'Scaffold the Auth0 hook code', 11 | optionGroups: { 12 | 'Hook scaffolding': { 13 | 'type': { 14 | alias: 't', 15 | description: 'Hook type, required. One of: ' + extensionTypes.join(', ') + '.', 16 | choices: extensionTypes, 17 | required: true, 18 | dest: 'extensionName', 19 | metavar: 'TYPE', 20 | type: 'string' 21 | }, 22 | } 23 | }, 24 | handler: (args) => console.log(auth0Extensions[args.extensionName].sample), 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /bin/auth0_toggle.js: -------------------------------------------------------------------------------- 1 | const Cli = require('structured-cli'); 2 | const Chalk = require('chalk'); 3 | 4 | 5 | module.exports = function (action) { 6 | return Cli.createCommand(action, { 7 | description: (action === 'enable' ? 'Enable' : 'Disable') + ' a hook', 8 | plugins: [ 9 | require('./_plugins/profile'), 10 | ], 11 | params: { 12 | 'name': { 13 | description: 'The name of the hook to ' + action + '.', 14 | type: 'string', 15 | required: true, 16 | } 17 | }, 18 | handler: createHandleUpdate(action), 19 | }); 20 | }; 21 | 22 | // Command handler 23 | 24 | function createHandleUpdate(action) { 25 | return function handleUpdate(args) { 26 | 27 | var profile = args.profile; 28 | 29 | return profile.inspectWebtask({ 30 | name: args.name, 31 | decrypt: true, 32 | meta: true 33 | }) 34 | .then(onClaims); 35 | 36 | function onClaims(claims) { 37 | // Set the user-defined options from the inspected webtask's claims 38 | if (!claims.meta || claims.meta['auth0-extension'] !== 'runtime') 39 | return Cli.error.invalid('The ' + args.name + ' webtask is not an Auth0 hook.'); 40 | var extensionName = claims.meta['auth0-extension-name']; 41 | if (!extensionName) 42 | return Cli.error.invalid('The ' + args.name + ' webtask is not a an Auth0 hook.'); 43 | if (action === 'enable' && claims.meta['auth0-extension-disabled'] !== "1") 44 | return console.log(Chalk.green('The ' + args.name + ' (' + extensionName + ') hook is already enabled.')); 45 | if (action === 'disable' && claims.meta['auth0-extension-disabled'] === "1") 46 | return console.log(Chalk.green('The ' + args.name + ' (' + extensionName + ') hook is already disabled.')); 47 | 48 | if (action === 'disable') { 49 | claims = adjustExtensionClaims(claims, false); 50 | return profile.createTokenRaw(claims) 51 | .then(function () { 52 | return console.log(Chalk.green('The ' + args.name + ' (' + extensionName + ') hook has been disabled.')); 53 | }); 54 | } 55 | else { // enable 56 | return profile.listWebtasks({ 57 | meta: { 58 | 'auth0-extension': 'runtime', 59 | 'auth0-extension-name': extensionName 60 | } 61 | }).then(function (webtasks) { 62 | var toDisable = []; 63 | webtasks.forEach(function (wt) { 64 | if (wt.meta['auth0-extension-disabled'] !== "1") { 65 | toDisable.push(toggleExtension(wt.toJSON().name, false)); 66 | } 67 | }); 68 | return Promise.all(toDisable); 69 | }).then(function () { 70 | claims = adjustExtensionClaims(claims, true); 71 | return profile.createTokenRaw(claims) 72 | .then(function () { 73 | return console.log(Chalk.green('The ' + args.name + ' (' + extensionName + ') hook has been enabled.')); 74 | }); 75 | }); 76 | } 77 | } 78 | 79 | function toggleExtension(name, enable) { 80 | return profile.inspectWebtask({ name, meta: true, decrypt: true }) 81 | .then(function (claims) { 82 | claims = adjustExtensionClaims(claims, enable); 83 | return profile.createTokenRaw(claims); 84 | }); 85 | } 86 | 87 | function adjustExtensionClaims(claims, enable) { 88 | if (enable) { 89 | console.log('Enabling hook ' + claims.jtn + '.'); 90 | claims.meta['auth0-extension-disabled'] = '0'; 91 | } 92 | else { 93 | console.log('Disabling hook ' + claims.jtn + '.'); 94 | claims.meta['auth0-extension-disabled'] = '1'; 95 | } 96 | ['jti','ca','iat','webtask_url'].forEach(function (c) { delete claims[c]; }); 97 | return claims; 98 | } 99 | }; 100 | } 101 | 102 | -------------------------------------------------------------------------------- /bin/browse.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var Open = require('opn'); 4 | 5 | 6 | module.exports = Cli.createCommand('browse', { 7 | description: 'Browse this webtask in your browser (HTTP GET)', 8 | plugins: [ 9 | require('./_plugins/profile'), 10 | ], 11 | params: { 12 | 'name': { 13 | description: 'The named webtask you want to browse', 14 | type: 'string', 15 | required: true 16 | }, 17 | }, 18 | handler: handleBrowse, 19 | }); 20 | 21 | 22 | // Command handler 23 | 24 | function handleBrowse(args) { 25 | var profile = args.profile; 26 | var wtName = args.name ? args.name + '/' : ''; 27 | var url = profile.url + '/api/run/' + profile.container + '/' + wtName; 28 | 29 | console.log('Browsing ' + Chalk.underline(args.name) + ' in your browser...'); 30 | 31 | Open(url, { wait: false }); 32 | } 33 | -------------------------------------------------------------------------------- /bin/create.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var CreateWebtask = require('../lib/createWebtask'); 4 | var ValidateCreateArgs = require('../lib/validateCreateArgs'); 5 | 6 | 7 | module.exports = Cli.createCommand('create', { 8 | description: 'Create and update webtasks', 9 | plugins: [ 10 | require('./_plugins/profile'), 11 | ], 12 | optionGroups: { 13 | 'Output options': { 14 | output: { 15 | alias: 'o', 16 | description: 'Set the output format', 17 | choices: ['json'], 18 | type: 'string', 19 | }, 20 | 'show-token': { 21 | description: 'Show tokens (hidden by default)', 22 | dest: 'showToken', 23 | type: 'boolean', 24 | }, 25 | }, 26 | 'Webtask creation': { 27 | 'secret': { 28 | action: 'append', 29 | alias: 's', 30 | defaultValue: [], 31 | description: 'Secret(s) exposed to your code as `secrets` on the webtask context object. These secrets will be encrypted and stored in a webtask token in such a way that only the webtask server is able to decrypt the secrets so that they may be exposed to your running webtask code.', 32 | dest: 'secrets', 33 | metavar: 'KEY=VALUE', 34 | type: 'string', 35 | }, 36 | 'secrets-file': { 37 | description: 'A file containing one secret per line in KEY=VALUE format', 38 | dest: 'secretsFile', 39 | metavar: 'FILENAME', 40 | type: 'string', 41 | }, 42 | 'param': { 43 | action: 'append', 44 | defaultValue: [], 45 | description: 'Param(s) exposed to your code as `params` on the webtask context object. The properties will be signed and protected from interference but not encrypted.', 46 | dest: 'params', 47 | metavar: 'KEY=VALUE', 48 | type: 'string', 49 | }, 50 | 'meta': { 51 | action: 'append', 52 | defaultValue: [], 53 | description: 'Metadata describing the webtask. This is a set of string key value pairs.', 54 | dest: 'meta', 55 | metavar: 'KEY=VALUE', 56 | type: 'string', 57 | }, 58 | 'meta-file': { 59 | description: 'A file containing one meta per line in KEY=VALUE format', 60 | dest: 'metaFile', 61 | metavar: 'FILENAME', 62 | type: 'string', 63 | }, 64 | 'name': { 65 | alias: 'n', 66 | description: 'Name of the webtask. When specified, the resulting webtask token can be run at a special named webtask url and additional path segments are allowed (/api/run/{container}/{name}/*). This is important when using `webtask-tools` to expose an Express server as a webtask.', 67 | type: 'string' 68 | }, 69 | 'dependency': { 70 | action: 'append', 71 | alias: 'd', 72 | defaultValue: [], 73 | description: 'Specify a dependency on a node module. The best matching version of this node module (at the time of webtask creation) will be available in your webtask code via `require()`. You can use this option more than once to add mutliple dependencies.', 74 | dest: 'dependencies', 75 | metavar: 'NAME@VERSION', 76 | type: 'string', 77 | }, 78 | 'ignore-package-json': { 79 | description: 'Ignore any dependencies found in a package.json file adjacent to your webtask.', 80 | dest: 'ignorePackageJson', 81 | type: 'boolean', 82 | }, 83 | 'middleware': { 84 | action: 'append', 85 | defaultValue: [], 86 | description: 'Specify a webtask middleware that should be run prior to (or in lieu of) your underlying webtask code. Middleware specified in this way will automatically be added as dependencies. Use of middleware implies the @webtask/middleware-compiler which will also automatically be added to dependencies. For more information on middleware, see: https://goo.gl/yh3VAB', 87 | metavar: 'NAME@VERSION/EXPORT or URL', 88 | type: 'string', 89 | }, 90 | 'watch': { 91 | alias: 'w', 92 | description: 'Automatically watch and reprovision the webtask on local file changes. This will also subscribe you to logs as if you had done `wt logs` to provide an intuitive development experience without requiring multiple active terminals.', 93 | type: 'boolean', 94 | }, 95 | 'no-merge': { 96 | action: 'storeFalse', 97 | defaultValue: true, 98 | description: 'Disable automatic merging of the parsed body and secrets into the `data` field of the webtask context object. The parsed body (if available) will be on the `body` field and secrets on the `secrets` field.', 99 | dest: 'merge', 100 | }, 101 | 'no-parse': { 102 | description: 'Deprecated and ignored.', 103 | type: 'boolean' 104 | }, 105 | 'no-transpile': { 106 | description: 'Disable transpilation (lowering) of code syntax during bundling.', 107 | dest: 'noTranspile', 108 | type: 'boolean', 109 | }, 110 | 'parse-body': { 111 | description: 'Automatically parse JSON and application/x-www-form-urlencoded request bodies. Use this with (ctx, req, res) webtask signatures if you want webtask runtime to parse the request body and store it in ctx.body.', 112 | type: 'boolean', 113 | dest: 'parseBody' 114 | }, 115 | 'bundle': { 116 | alias: 'b', 117 | description: 'Use `webtask-bundle` to bundle your code into a single file. This tool can compile ES2015 (ES6) code via Babel as well as packaging up a webtask composed of multiple files into a single file. The tool will scan your package.json for dependencies and will automatically bundle those that are not available on the webtask platform. Enabling --bundle-loose will prevent this check from doing strict semver range comparisons on dependencies.', 118 | type: 'boolean', 119 | }, 120 | 'bundle-minify': { 121 | description: 'Generate a minified production build', 122 | type: 'boolean', 123 | dest: 'minify' 124 | }, 125 | 'bundle-strict': { 126 | description: 'Enforce strict semver matching for bundling with `webtask-bundle`', 127 | dest: 'loose', 128 | action: 'storeFalse', 129 | defaultValue: true, 130 | type: 'boolean', 131 | }, 132 | 'capture': { 133 | description: 'Download and use the current code indicated by `url`. When you are developing a webtask whose code is remotely hosted, this option will automatically download the remote code before creating the webtask. This means that the webtask will continue to run even if the remote url becomes unavailable.', 134 | type: 'boolean', 135 | }, 136 | 'prod': { 137 | description: 'Deprecated and ignored.', 138 | type: 'boolean', 139 | }, 140 | 'host': { 141 | description: 'Allow the webtask to be called using a custom domain name. Using this option requires proof of domain ownership. This can be done by adding a TXT record type to the DNS of the chosen domain. The value of the record must be `webtask:container:{container}`, where {container} is the webtask container name to be associated with the custom domain. Many such TXT records can be created as needed.', 142 | type: 'string' 143 | }, 144 | }, 145 | }, 146 | params: { 147 | 'file_or_url': { 148 | description: 'Path or URL of the webtask\'s code. When not specified, code will be read from STDIN.', 149 | type: 'string', 150 | }, 151 | }, 152 | epilog: Chalk.underline('Sample usage:') + '\n' 153 | + '1. Create a basic webtask:' + '\n' 154 | + Chalk.bold(' $ wt create ./sample-webtasks/hello-world.js') + '\n' 155 | + '\n' 156 | + '2. Create a webtask with one secret:' + '\n' 157 | + Chalk.bold(' $ wt create --secret name=webtask ./sample-webtasks/html-response.js') + '\n' 158 | + '\n' 159 | + '3. Create a webtask that is bundled before deploying:' + '\n' 160 | + Chalk.bold(' $ wt create --secret name=webtask --bundle ./sample-webtasks/bundled-webtask.js') + '\n' 161 | , 162 | handler: handleCreate, 163 | }); 164 | 165 | 166 | // Command handler 167 | 168 | function handleCreate(args) { 169 | args = ValidateCreateArgs(args); 170 | 171 | return CreateWebtask(args, { action: 'created' }); 172 | } 173 | -------------------------------------------------------------------------------- /bin/cron.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | 3 | 4 | var category = module.exports = Cli.createCategory('cron', { 5 | description: 'Manage scheduled webtasks', 6 | }); 7 | 8 | category.addChild(require('./cron/create')); 9 | category.addChild(require('./cron/ls')); 10 | category.addChild(require('./cron/get')); 11 | category.addChild(require('./cron/rm')); 12 | category.addChild(require('./cron/history')); 13 | category.addChild(require('./cron/reschedule')); 14 | category.addChild(require('./cron/update')); 15 | category.addChild(require('./cron/schedule')); 16 | -------------------------------------------------------------------------------- /bin/cron/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Cli = require('structured-cli'); 4 | const Cron = require('../../lib/cron'); 5 | const Logs = require('../../lib/logs'); 6 | const PrintCronJob = require('../../lib/printCronJob'); 7 | const ValidateCreateArgs = require('../../lib/validateCreateArgs'); 8 | const WebtaskCreator = require('../../lib/webtaskCreator'); 9 | const _ = require('lodash'); 10 | 11 | const CRON_AUTH_MIDDLEWARE = '@webtask/cron-auth-middleware'; 12 | const CRON_AUTH_MIDDLEWARE_VERSION = '^1.2.1'; 13 | 14 | const createCommand = require('../create'); 15 | 16 | module.exports = Cli.createCommand('create', { 17 | description: 'Create a cron webtask', 18 | plugins: [require('../_plugins/profile')], 19 | optionGroups: _.extend({}, createCommand.optionGroups, { 20 | 'Cron options': { 21 | 'no-auth': { 22 | description: 'Disable cron webtask authentication', 23 | dest: 'noAuth', 24 | type: 'boolean', 25 | }, 26 | schedule: { 27 | description: 28 | 'Either a cron-formatted schedule (see: http://crontab.guru/) or an interval of hours ("h") or minutes ("m"). Note that not all intervals are possible.', 29 | type: 'string', 30 | required: true, 31 | }, 32 | state: { 33 | description: "Set the cron job's state", 34 | choices: ['active', 'inactive'], 35 | defaultValue: 'active', 36 | type: 'string', 37 | }, 38 | tz: { 39 | description: `An IANA timezone name (see: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), relative to which the cron schedule will be calculated. If not specified, the webtask cluster's default timezone is used.`, 40 | metavar: 'TIMEZONE', 41 | type: 'string', 42 | }, 43 | }, 44 | }), 45 | options: _.extend({}, createCommand.options, {}), 46 | params: createCommand.params, 47 | epilog: [ 48 | 'Examples:', 49 | '1. Create a webtask that runs every five minutes:', 50 | ' $ wt cron create --schedule 5m ./sample-webtasks/hello-world.js', 51 | '2. Create a webtask that runs every Tuesday at 09:05:', 52 | ' $ wt cron create --schedule "5 9 * * 2" ./sample-webtasks/hello-world.js', 53 | '3. Create a webtask that runs every Tuesday at 09:05 UTC:', 54 | ' $ wt cron create --schedule "5 9 * * 2" --tz UTC ./sample-webtasks/hello-world.js', 55 | ].join('\n'), 56 | handler: handleCronCreate, 57 | }); 58 | 59 | // Command handler 60 | 61 | function handleCronCreate(args) { 62 | const profile = args.profile; 63 | 64 | if (!args.noAuth) { 65 | args.middleware.push( 66 | `${CRON_AUTH_MIDDLEWARE}@${CRON_AUTH_MIDDLEWARE_VERSION}` 67 | ); 68 | } 69 | 70 | const tz = args.tz ? Cron.parseTimezone(args.tz): undefined; 71 | const schedule = Cron.parseSchedule(args.schedule); 72 | 73 | args = ValidateCreateArgs(args); 74 | 75 | const createWebtask = WebtaskCreator(args, { 76 | onGeneration: onGeneration, 77 | }); 78 | const logger = createLogger(args, profile); 79 | 80 | return createWebtask(profile); 81 | 82 | function onGeneration(build) { 83 | if (args.watch) { 84 | logger.log( 85 | { 86 | generation: build.generation, 87 | container: build.webtask.container, 88 | }, 89 | 'Webtask created: %s. Scheduling cron job...', 90 | build.webtask.url 91 | ); 92 | } 93 | 94 | return build.webtask 95 | .createCronJob({ 96 | schedule, 97 | tz, 98 | state: args.state, 99 | meta: args.meta, 100 | }) 101 | .then(onCronScheduled, onCronError); 102 | 103 | function onCronScheduled(job) { 104 | args.watch 105 | ? logger.log( 106 | { 107 | generation: build.generation, 108 | container: job.container, 109 | state: job.state, 110 | schedule: job.schedule, 111 | timezone: job.tz, 112 | next_available_at: new Date( 113 | job.next_available_at 114 | ).toLocaleString(), 115 | created_at: new Date(job.created_at).toLocaleString(), 116 | run_count: job.run_count, 117 | error_count: job.error_count, 118 | meta: job.meta, 119 | }, 120 | 'Cron job scheduled' 121 | ) 122 | : PrintCronJob(job, logger); 123 | } 124 | 125 | function onCronError(err) { 126 | switch (err.statusCode) { 127 | case 400: 128 | throw Cli.error.invalid( 129 | 'Invalid cron job; please check that the schedule is a valid cron schedule' 130 | ); 131 | default: 132 | throw err; 133 | } 134 | } 135 | } 136 | } 137 | 138 | function createLogger(args, profile) { 139 | if (args.watch) { 140 | const logs = Logs.createLogStream(profile); 141 | 142 | return { 143 | log: _.bindKey(logs, 'info'), 144 | error: _.bindKey(logs, 'error'), 145 | }; 146 | } else { 147 | return { 148 | log: _.bindKey(console, 'log'), 149 | error: _.bindKey(console, 'error'), 150 | }; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /bin/cron/get.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | var PrintCronJob = require('../../lib/printCronJob'); 3 | 4 | 5 | module.exports = Cli.createCommand('get', { 6 | description: 'Get information about a scheduled webtask', 7 | plugins: [ 8 | require('../_plugins/profile'), 9 | ], 10 | options: { 11 | output: { 12 | alias: 'o', 13 | description: 'Set the output format.', 14 | choices: ['json'], 15 | type: 'string', 16 | } 17 | }, 18 | params: { 19 | name: { 20 | description: 'Name of the cron job to inspect.', 21 | type: 'string', 22 | required: true, 23 | } 24 | }, 25 | handler: handleCronGet, 26 | }); 27 | 28 | 29 | // Command handler 30 | 31 | function handleCronGet(args) { 32 | var profile = args.profile; 33 | 34 | return profile.getCronJob({ container: args.container || profile.container, name: args.name }) 35 | .then(onCronJob, onCronError); 36 | 37 | 38 | function onCronJob(job) { 39 | if (args.output === 'json') { 40 | console.log(JSON.stringify(job, null, 2)); 41 | } else { 42 | PrintCronJob(job); 43 | } 44 | } 45 | 46 | function onCronError(err) { 47 | switch (err.statusCode) { 48 | case 404: throw Cli.error.notFound('No such webtask: ' + args.name); 49 | default: throw err; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bin/cron/history.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | var Pad = require('pad'); 3 | var _ = require('lodash'); 4 | 5 | 6 | module.exports = Cli.createCommand('history', { 7 | description: 'Review cron job history', 8 | plugins: [ 9 | require('../_plugins/profile'), 10 | ], 11 | optionGroups: { 12 | 'Output options': { 13 | output: { 14 | alias: 'o', 15 | description: 'Set the output format', 16 | choices: ['json'], 17 | type: 'string', 18 | }, 19 | fields: { 20 | description: 'Only print the indicated fields (comma-separated list).', 21 | defaultValue: 'scheduled_at,started_at,completed_at,type,statusCode,body', 22 | type: 'string', 23 | }, 24 | }, 25 | 'Pagination': { 26 | offset: { 27 | description: 'Skip this many history entries.', 28 | type: 'int', 29 | defaultValue: 0, 30 | }, 31 | limit: { 32 | description: 'Limit the result-set to this many entries.', 33 | type: 'int', 34 | defaultValue: 20, 35 | }, 36 | }, 37 | }, 38 | params: { 39 | name: { 40 | description: 'Name of the cron job to inspect.', 41 | type: 'string', 42 | required: true, 43 | } 44 | }, 45 | handler: handleCronHistory, 46 | }); 47 | 48 | 49 | // Command handler 50 | 51 | function handleCronHistory(args) { 52 | var profile = args.profile; 53 | 54 | return profile.getCronJobHistory({ 55 | container: args.container || profile.container, 56 | name: args.name, 57 | offset: args.offset, 58 | limit: args.limit, 59 | }) 60 | .then(onCronJobHistory, onCronError); 61 | 62 | 63 | function onCronJobHistory(results) { 64 | if (args.output === 'json') { 65 | console.log(JSON.stringify(results, null, 2)); 66 | } else { 67 | var fields = args.fields.split(/\s*,\s*/).filter(Boolean); 68 | 69 | _.forEach(results, function (result, i) { 70 | if (i) console.log(); 71 | 72 | printCronResult(result, fields); 73 | }); 74 | } 75 | } 76 | 77 | function onCronError(err) { 78 | switch (err.statusCode) { 79 | case 404: throw Cli.error.notFound('No such webtask: ' + args.name); 80 | default: throw err; 81 | } 82 | } 83 | } 84 | 85 | 86 | function printCronResult(result, fields) { 87 | _.forEach(fields, function (field) { 88 | var value = result[field]; 89 | 90 | if (value) console.log(Pad(field + ':', 18), value); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /bin/cron/ls.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var PrintCronJob = require('../../lib/printCronJob'); 4 | var _ = require('lodash'); 5 | var keyValList2Object = require('../../lib/keyValList2Object'); 6 | 7 | 8 | module.exports = Cli.createCommand('ls', { 9 | description: 'List scheduled webtasks', 10 | plugins: [ 11 | require('../_plugins/profile'), 12 | ], 13 | optionGroups: { 14 | 'Filtering': { 15 | 'meta': { 16 | action: 'append', 17 | defaultValue: [], 18 | description: 'Metadata describing the scheduled webtask. This is a set of string key value pairs. Only scheduled webtasks with matching metadata will be returned.', 19 | dest: 'meta', 20 | metavar: 'KEY=VALUE', 21 | type: 'string', 22 | }, 23 | 24 | }, 25 | 'Output options': { 26 | output: { 27 | alias: 'o', 28 | description: 'Set the output format', 29 | choices: ['json'], 30 | type: 'string', 31 | }, 32 | }, 33 | }, 34 | handler: handleCronLs, 35 | }); 36 | 37 | 38 | // Command handler 39 | 40 | function handleCronLs(args) { 41 | var profile = args.profile; 42 | 43 | keyValList2Object(args, 'meta'); 44 | 45 | return profile.listCronJobs({ container: args.container || profile.container, meta: args.meta }) 46 | .then(onCronListing); 47 | 48 | 49 | function onCronListing(jobs) { 50 | if (args.output === 'json') { 51 | console.log(JSON.stringify(jobs, null, 2)); 52 | } else { 53 | 54 | jobs.forEach(function (job, i) { 55 | if (i) console.log(); // Separator line 56 | 57 | PrintCronJob(job); 58 | }); 59 | 60 | if (!jobs.length) { 61 | console.log('No scheduled webtasks found. To create one:'); 62 | console.log(Chalk.bold('$ wt cron schedule [options] "* * * * *" [file_or_url]')); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /bin/cron/reschedule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Cli = require('structured-cli'); 4 | const Cron = require('../../lib/cron'); 5 | const PrintCronJob = require('../../lib/printCronJob'); 6 | 7 | module.exports = Cli.createCommand('reschedule', { 8 | description: 'Change the timing of a cron job', 9 | plugins: [require('../_plugins/profile')], 10 | optionGroups: { 11 | 'Cron options': { 12 | schedule: { 13 | description: 14 | 'Either a cron-formatted schedule (see: http://crontab.guru/) or an interval of hours ("h") or minutes ("m"). Note that not all intervals are possible.', 15 | type: 'string', 16 | }, 17 | state: { 18 | description: "Set the cron job's state", 19 | choices: ['active', 'inactive'], 20 | type: 'string', 21 | }, 22 | tz: { 23 | description: `An IANA timezone name (see: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), relative to which the cron schedule will be calculated. If not specified, the webtask cluster's default timezone is used.`, 24 | metavar: 'TIMEZONE', 25 | type: 'string', 26 | }, 27 | }, 28 | }, 29 | params: { 30 | name: { 31 | description: 'The name of the cron job.', 32 | type: 'string', 33 | required: true, 34 | }, 35 | }, 36 | handler: handleCronReschedule, 37 | }); 38 | 39 | // Command handler 40 | 41 | function handleCronReschedule(args) { 42 | const profile = args.profile; 43 | const updateOptions = { 44 | name: args.name, 45 | }; 46 | 47 | if (args.schedule) 48 | updateOptions.schedule = Cron.parseSchedule(args.schedule); 49 | if (args.state) updateOptions.state = args.state; 50 | if (args.tz) updateOptions.tz = Cron.parseTimezone(args.tz); 51 | 52 | return profile 53 | .getCronJob({ name: args.name }) 54 | .then(cronJob => 55 | profile.createCronJob({ 56 | meta: cronJob.meta, 57 | name: args.name, 58 | schedule: args.schedule 59 | ? Cron.parseSchedule(args.schedule) 60 | : cronJob.schedule, 61 | state: args.state || cronJob.state, 62 | token: cronJob.token, 63 | tz: args.tz ? Cron.parseTimezone(args.tz) : cronJob.tz, 64 | }) 65 | ) 66 | .tap(cronJob => PrintCronJob(cronJob)); 67 | } 68 | -------------------------------------------------------------------------------- /bin/cron/rm.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | var _ = require('lodash'); 3 | 4 | 5 | module.exports = Cli.createCommand('rm', { 6 | description: 'Remove a scheduled webtask', 7 | plugins: [ 8 | require('../_plugins/profile'), 9 | ], 10 | options: { 11 | output: { 12 | alias: 'o', 13 | description: 'Set the output format', 14 | choices: ['json'], 15 | type: 'string', 16 | } 17 | }, 18 | params: { 19 | name: { 20 | description: 'Name of the cron job', 21 | type: 'string', 22 | required: true, 23 | } 24 | }, 25 | handler: handleCronRm, 26 | }); 27 | 28 | 29 | // Command handler 30 | 31 | function handleCronRm(args) { 32 | var profile = args.profile; 33 | 34 | return profile.removeCronJob({ container: args.container || profile.container, name: args.name }) 35 | .then(onCronJobRemoved, onCronError); 36 | 37 | 38 | function onCronJobRemoved() { 39 | if (args.output === 'json') { 40 | console.log(true); 41 | } else { 42 | console.log('Successfully removed the scheduled webtask: %s', args.name); 43 | } 44 | } 45 | 46 | function onCronError(err) { 47 | switch (err.statusCode) { 48 | case 404: throw Cli.error.notFound('No such webtask: ' + args.name); 49 | default: throw err; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bin/cron/schedule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Cli = require('structured-cli'); 4 | 5 | module.exports = Cli.createCommand('schedule', { 6 | description: `[DEPRECATED] Please use 'wt cron create' or 'wt cron update'`, 7 | handler: handleCronSchedule, 8 | }); 9 | 10 | // Command handler 11 | 12 | function handleCronSchedule() { 13 | throw new Cli.error.invalid( 14 | `The 'wt cron schedule' command has been deprecated in favor of 'wt cron create' and 'wt cron update'.` 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /bin/cron/update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Cli = require('structured-cli'); 4 | const PrintCronJob = require('../../lib/printCronJob'); 5 | const ValidateCreateArgs = require('../../lib/validateCreateArgs'); 6 | const WebtaskCreator = require('../../lib/webtaskCreator'); 7 | 8 | const updateCommand = require('../update'); 9 | 10 | module.exports = Cli.createCommand('update', { 11 | description: 'Update the code of a cron webtask', 12 | plugins: [require('../_plugins/profile')], 13 | optionGroups: updateCommand.optionGroups, 14 | options: updateCommand.options, 15 | params: updateCommand.params, 16 | handler: handleCronUpdate, 17 | }); 18 | 19 | // Command handler 20 | 21 | function handleCronUpdate(args) { 22 | args = ValidateCreateArgs(args); 23 | 24 | const profile = args.profile; 25 | 26 | return profile.getCronJob({ name: args.name }).then(cronJob => 27 | profile 28 | .inspectToken({ token: cronJob.token, decrypt: true, meta: true }) 29 | .then(claims => new Promise((resolve, reject) => { 30 | // Set the user-defined options from the inspected webtask's claims 31 | args.host = claims.host; 32 | args.merge = claims.mb; 33 | args.parse = claims.pb; 34 | args.secrets = claims.ectx; 35 | args.params = claims.pctx; 36 | args.meta = claims.meta; 37 | 38 | const createWebtask = WebtaskCreator(args, { 39 | onError: error => reject(error), 40 | onGeneration: build => resolve(build.webtask), 41 | }); 42 | 43 | return createWebtask(profile); 44 | })) 45 | .then(webtask => 46 | webtask.createCronJob({ 47 | schedule: cronJob.schedule, 48 | state: cronJob.state, 49 | meta: cronJob.meta, 50 | tz: cronJob.tz, 51 | }) 52 | ) 53 | .tap(cronJob => PrintCronJob(cronJob)) 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /bin/debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Bluebird = require('bluebird'); 4 | const Cli = require('structured-cli'); 5 | const spawn = require('child_process').spawn; 6 | const _ = require('lodash'); 7 | const config = require('./serveCommon')(); 8 | var newArgs = null; 9 | 10 | config.description = "Debug a webtask"; 11 | config.handler = handleDebug; 12 | config.optionGroups.ExecutionOptions = 13 | { 14 | 'debugger': { 15 | alias: 'd', 16 | description: 'Debugger to use. "devtool" requires installing the devtool cli (npm install devtool -g)', 17 | choices: ['devtool', 'node'], 18 | dest: 'debugger', 19 | defaultValue: 'node' 20 | } 21 | }; 22 | 23 | module.exports = Cli.createCommand('debug', config); 24 | 25 | function handleDebug(args) { 26 | var skip = false; 27 | newArgs = [process.argv[1], 'serve']; 28 | 29 | _.each(process.argv.slice(3), (value) => { 30 | if (!skip) { 31 | var arg = value.toLowerCase(); 32 | if (arg === '-d' || arg === '--debugger') { 33 | // next arg will be debugger parameter, so skip it 34 | skip = true; 35 | return; 36 | } 37 | if (!arg.startsWith('--debugger=') && !arg.startsWith('-d=')) { 38 | newArgs.push(value); 39 | } 40 | } else { 41 | skip = false; 42 | } 43 | }); 44 | if (args.debugger === 'node') { 45 | return new Bluebird(debugNode); 46 | } 47 | else if (args.debugger === 'devtool') { 48 | return new Bluebird(debugDevtool); 49 | } 50 | } 51 | 52 | function debugNode(resolve, reject) { 53 | const version = parseInt(process.version.replace('v', '')); 54 | if(version < 8) { 55 | newArgs = ['--debug'].concat(newArgs); 56 | } else { 57 | newArgs = ['--inspect'].concat(newArgs); 58 | } 59 | spawnProcess(process.execPath, newArgs, resolve); 60 | } 61 | 62 | function debugDevtool(resolve, reject) { 63 | spawnProcess('devtool', newArgs, resolve); 64 | } 65 | 66 | function spawnProcess(launcher, args, resolve) { 67 | var node = spawn(launcher, args); 68 | 69 | node.stdout.on('data', (data) => { 70 | console.log(`${data}`); 71 | }); 72 | 73 | node.stderr.on('data', (data) => { 74 | console.error(`${data}`); 75 | }); 76 | 77 | node.on('close', (code) => { 78 | resolve(); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /bin/edit.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var Open = require('opn'); 4 | 5 | 6 | module.exports = Cli.createCommand('edit', { 7 | description: 'Edit this webtask in your browser', 8 | plugins: [ 9 | require('./_plugins/profile'), 10 | ], 11 | params: { 12 | 'name': { 13 | description: 'The named webtask you want to edit', 14 | type: 'string', 15 | required: false 16 | }, 17 | }, 18 | optionGroups: { 19 | 'Editor options': { 20 | 'editor-version': { 21 | description: 'Open the webtask in a specific version of the Webtask Editor', 22 | dest: 'editorVersion', 23 | choices: ['v1', 'v2'], 24 | type: 'string', 25 | }, 26 | 'canary': { 27 | description: 'Open the canary build of the Webtask Editor', 28 | dest: 'canary', 29 | type: 'boolean', 30 | }, 31 | } 32 | }, 33 | handler: handleEdit, 34 | }); 35 | 36 | 37 | // Command handler 38 | function handleEdit(args) { 39 | var profile = args.profile; 40 | var wtName = args.name ? args.name + '/' : ''; 41 | var url = profile.url + '/edit/' + profile.container + '#/' + wtName + profile.token; 42 | 43 | if (args.editorVersion) { 44 | url = profile.url + '/edit/' + args.editorVersion + '/' + profile.container + '#/' + wtName + profile.token; 45 | } 46 | 47 | if (args.canary) { 48 | url = profile.url + '/edit/canary/' + profile.container + '#/' + wtName + profile.token; 49 | } 50 | 51 | console.log('Attempting to open the following url in your browser: '); 52 | console.log(); 53 | console.log(Chalk.underline(url)); 54 | console.log(); 55 | console.log('If the webtask editor does not automatically open, please copy this address and paste it into your browser.'); 56 | 57 | return Open(url, { wait: false }); 58 | } -------------------------------------------------------------------------------- /bin/init.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./profile/init'); -------------------------------------------------------------------------------- /bin/inspect.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./token/inspect'); -------------------------------------------------------------------------------- /bin/logs.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Cli = require('structured-cli'); 3 | var Logs = require('../lib/logs'); 4 | var _ = require('lodash'); 5 | 6 | 7 | module.exports = Cli.createCommand('logs', { 8 | description: 'Streaming, real-time logs', 9 | plugins: [ 10 | require('./_plugins/profile'), 11 | ], 12 | optionGroups: { 13 | 'Log options': { 14 | raw: { 15 | alias: 'r', 16 | description: 'Do not pretty print', 17 | type: 'boolean', 18 | }, 19 | verbose: { 20 | alias: 'v', 21 | description: 'Show verbose logs', 22 | type: 'boolean', 23 | }, 24 | }, 25 | browser: { 26 | alias: 'b', 27 | description: 'create URL to see logs in a browser', 28 | type: 'boolean', 29 | }, 30 | }, 31 | handler: handleLogs, 32 | }); 33 | 34 | 35 | // Command handler 36 | 37 | function handleLogs(args) { 38 | var profile = args.profile; 39 | 40 | return new Bluebird((resolve, reject) => { 41 | const logs = profile.createLogStream({ json: true }); 42 | const logger = Logs.createLogStream(logs, Object.assign({ 43 | container: profile.container, 44 | }, args)); 45 | const timeout = setTimeout(() => { 46 | const error = Cli.error.timeout('Automatically disconnecting from logs after 30min'); 47 | 48 | return reject(error); 49 | }, 30 * 60 * 1000); 50 | 51 | logs.once('close', () => { 52 | clearTimeout(timeout); 53 | 54 | const error = Cli.error.cancelled('Connection to streaming log endpoint lost'); 55 | 56 | return reject(error); 57 | }); 58 | 59 | logs.once('error', (error) => { 60 | logger.error(error.message); 61 | 62 | clearTimeout(timeout); 63 | 64 | return reject(Cli.error.serverError(`Error connecting to streaming log endpoint: ${ error.message }`)); 65 | }); 66 | 67 | process.once('SIGINT', () => { 68 | logger.warn('Received SIGINT; disconnecting from logs'); 69 | return resolve(); 70 | }); 71 | }); 72 | } 73 | 74 | -------------------------------------------------------------------------------- /bin/ls.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var PrintWebtask = require('../lib/printWebtask'); 4 | var _ = require('lodash'); 5 | var keyValList2Object = require('../lib/keyValList2Object'); 6 | 7 | 8 | module.exports = Cli.createCommand('ls', { 9 | description: 'List named webtasks', 10 | plugins: [ 11 | require('./_plugins/profile'), 12 | ], 13 | handler: handleTokenCreate, 14 | optionGroups: { 15 | 'Pagination': { 16 | 'offset': { 17 | type: 'int', 18 | description: 'Skip this many named webtasks', 19 | defaultValue: 0, 20 | }, 21 | 'limit': { 22 | type: 'int', 23 | description: 'Limit the results to this many named webtasks', 24 | defaultValue: 100, 25 | }, 26 | }, 27 | 'Filtering': { 28 | 'meta': { 29 | action: 'append', 30 | defaultValue: [], 31 | description: 'Metadata describing the webtask. This is a set of string key value pairs. Only webtasks with matching metadata will be returned.', 32 | dest: 'meta', 33 | metavar: 'KEY=VALUE', 34 | type: 'string', 35 | }, 36 | 37 | }, 38 | 'Output options': { 39 | 'output': { 40 | alias: 'o', 41 | description: 'Set the output format', 42 | choices: ['json'], 43 | type: 'string', 44 | }, 45 | 'details': { 46 | alias: 'd', 47 | description: 'Show more details', 48 | type: 'boolean', 49 | }, 50 | 'show-token': { 51 | description: 'Show the webtask tokens', 52 | dest: 'showToken', 53 | type: 'boolean', 54 | }, 55 | 'verbose': { 56 | description: 'Display full details', 57 | dest: 'verbose', 58 | type: 'boolean', 59 | } 60 | 61 | }, 62 | }, 63 | }); 64 | 65 | 66 | // Command handler 67 | 68 | function handleTokenCreate(args) { 69 | var profile = args.profile; 70 | 71 | keyValList2Object(args, 'meta'); 72 | 73 | return profile.listWebtasks({ offset: args.offset, limit: args.limit, meta: args.meta }) 74 | .catch(function (err) { 75 | if (err.statusCode >= 500) throw Cli.error.serverError(err.message); 76 | if (err.statusCode >= 400) throw Cli.error.badRequest(err.message); 77 | 78 | throw err; 79 | }) 80 | .then(onWebtasks); 81 | 82 | 83 | function onWebtasks(webtasks) { 84 | if (args.output === 'json') { 85 | var output = webtasks.map(function (webtask) { 86 | var json = webtask.toJSON(); 87 | var record = { 88 | container: json.container, 89 | name: json.name, 90 | url: webtask.url, 91 | }; 92 | if (webtask.meta) { 93 | record.meta = webtask.meta; 94 | } 95 | 96 | if (args.showToken) record.token = json.token; 97 | 98 | return record; 99 | }); 100 | 101 | console.log(JSON.stringify(output, null, 2)); 102 | } else { 103 | _.forEach(webtasks, function (webtask) { 104 | PrintWebtask(webtask, { details: args.details, token: args.showToken, verbose: args.verbose }); 105 | console.log(); 106 | }); 107 | 108 | if (!webtasks.length) { 109 | if (args.offset) { 110 | console.log('You have fewer than %s named webtasks.', Chalk.bold(args.offset)); 111 | } else { 112 | console.log(Chalk.green('You do not have any named webtasks. To get started, try:\n\n' 113 | + Chalk.bold('$ echo "module.exports = function (cb) { cb(null, \'Hello\'); }" > hello.js\n') 114 | + Chalk.bold('$ wt create hello.js\n'))); 115 | } 116 | } else { 117 | if (webtasks.length === args.limit) { 118 | console.log(Chalk.green('Successfully listed named webtasks %s to %s.\nTo list more try:'), Chalk.bold(args.offset + 1), Chalk.bold(args.offset + webtasks.length)); 119 | console.log(Chalk.bold('$ wt ls --offset %d'), args.offset + args.limit); 120 | } else { 121 | console.log(Chalk.green('Successfully listed named webtasks %s to %s.'), Chalk.bold(args.offset + 1), Chalk.bold(args.offset + webtasks.length)); 122 | } 123 | console.log(Chalk.green('Use --verbose to display full details.')); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /bin/modules.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | 3 | 4 | var category = module.exports = Cli.createCategory('modules', { 5 | description: 'Manage and search modules available on the platform', 6 | }); 7 | 8 | category.addChild(require('./modules/add')); 9 | category.addChild(require('./modules/versions')); 10 | -------------------------------------------------------------------------------- /bin/modules/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Bluebird = require('bluebird'); 4 | const Chalk = require('chalk'); 5 | const Cli = require('structured-cli'); 6 | const Modules = require('../../lib/modules'); 7 | 8 | 9 | module.exports = Cli.createCommand('add', { 10 | description: 'Add modules to the webtask platform', 11 | profileOptions: { 12 | hide: ['container'], 13 | }, 14 | plugins: [ 15 | require('../_plugins/profile'), 16 | ], 17 | handler: handleModulesAdd, 18 | optionGroups: { 19 | 'Modules options': { 20 | 'env': { 21 | type: 'string', 22 | defaultValue: 'node', 23 | choices: ['node'], 24 | description: 'Select the runtime for modules', 25 | }, 26 | 'rebuild': { 27 | alias: 'r', 28 | type: 'boolean', 29 | description: 'Queue the modules for rebuilding by the platform (administrator only)', 30 | dest: 'reset', 31 | }, 32 | 'wait': { 33 | alias: 'w', 34 | type: 'boolean', 35 | description: 'Wait for the modules to be available', 36 | }, 37 | }, 38 | 'Output options': { 39 | output: { 40 | alias: 'o', 41 | description: 'Set the output format', 42 | choices: ['json'], 43 | type: 'string', 44 | }, 45 | }, 46 | }, 47 | params: { 48 | 'name@version': { 49 | description: 'A list of modules and versions to add to the platform', 50 | type: 'string', 51 | nargs: '+', 52 | }, 53 | }, 54 | }); 55 | 56 | 57 | // Command handler 58 | 59 | function handleModulesAdd(args) { 60 | const specs = args['name@version']; 61 | const profile = args.profile; 62 | const stateToColor = { 63 | queued: Chalk.blue, 64 | available: Chalk.green, 65 | failed: Chalk.red, 66 | }; 67 | 68 | return Bluebird 69 | .map(specs, Modules.parseSpec) 70 | .tap(specs => { 71 | console.log('Adding the following modules to the platform:'); 72 | specs.forEach(spec => { 73 | console.log(` ${spec.name}@${spec.range}`); 74 | }); 75 | }) 76 | .map(spec => Modules.resolveSpec(profile, spec)) 77 | .then(modules => args.wait 78 | ? Modules.awaitAvailable(profile, modules, { reset: args.reset }) 79 | : Modules.ensure(profile, modules, { reset: args.reset })) 80 | .tap(modules => { 81 | console.log(Chalk.bold('Modules added:')); 82 | 83 | modules.forEach(module => { 84 | const color = stateToColor[module.state] || Chalk.white; 85 | 86 | console.log(` ${Chalk.bold(module.name)}@${Chalk.bold(module.version)}: ${color(module.state)}`); 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /bin/modules/versions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Chalk = require('chalk'); 4 | const Cli = require('structured-cli'); 5 | const Semver = require('semver'); 6 | 7 | 8 | module.exports = Cli.createCommand('versions', { 9 | description: 'List the versions of of a module that are currently available on the platform', 10 | profileOptions: { 11 | hide: ['container'], 12 | }, 13 | plugins: [ 14 | require('../_plugins/profile'), 15 | ], 16 | handler: handleModuleVersions, 17 | optionGroups: { 18 | 'Modules options': { 19 | 'env': { 20 | type: 'string', 21 | defaultValue: 'node', 22 | choices: ['node'], 23 | description: 'Select the runtime for modules', 24 | }, 25 | }, 26 | 'Output options': { 27 | output: { 28 | alias: 'o', 29 | description: 'Set the output format', 30 | choices: ['json'], 31 | type: 'string', 32 | }, 33 | }, 34 | }, 35 | params: { 36 | name: { 37 | description: 'The name of an npm module', 38 | type: 'string', 39 | required: true, 40 | }, 41 | }, 42 | }); 43 | 44 | 45 | // Command handler 46 | 47 | function handleModuleVersions(args) { 48 | const profile = args.profile; 49 | const modules$ = profile.listNodeModuleVersions({ name: args.name }); 50 | const stateToColor = { 51 | queued: Chalk.blue, 52 | available: Chalk.green, 53 | failed: Chalk.red, 54 | }; 55 | 56 | console.error(`Searching for the versions of ${Chalk.bold(args.name)} that are already available on the platform...`); 57 | 58 | return modules$ 59 | .then(modules => modules.sort((a, b) => Semver.rcompare(a.version, b.version))) 60 | .then(onListing); 61 | 62 | 63 | function onListing(modules) { 64 | if (args.output === 'json') { 65 | console.log(JSON.stringify(modules, null, 2)); 66 | return; 67 | } 68 | 69 | if (!modules.length) { 70 | console.log(Chalk.green(`No versions found for the module: ${Chalk.bold(args.name)}`)); 71 | return; 72 | } 73 | 74 | const versionsCount = modules.length; 75 | const versionsPluralization = versionsCount === 1 ? 'version' : 'versions'; 76 | 77 | console.error(Chalk.green(`We found ${Chalk.bold(versionsCount)} ${versionsPluralization} for the module: ${Chalk.bold(args.name)}`)); 78 | 79 | modules.forEach(module => { 80 | const color = stateToColor[module.state] || Chalk.white; 81 | console.log(`${Chalk.bold(module.version)}: ${color(module.state)}`); 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bin/profile.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | 3 | 4 | var category = module.exports = Cli.createCategory('profile', { 5 | description: 'Manage webtask profiles', 6 | }); 7 | 8 | category.addChild(require('./profile/init')); 9 | category.addChild(require('./profile/ls')); 10 | category.addChild(require('./profile/get')); 11 | category.addChild(require('./profile/rm')); 12 | category.addChild(require('./profile/nuke')); 13 | -------------------------------------------------------------------------------- /bin/profile/get.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var ConfigFile = require('../../lib/config'); 4 | var PrintProfile = require('../../lib/printProfile'); 5 | var _ = require('lodash'); 6 | 7 | 8 | 9 | module.exports = Cli.createCommand('get', { 10 | description: 'Get information about an existing webtask profile', 11 | handler: handleProfileGet, 12 | optionGroups: { 13 | 'Output options': { 14 | output: { 15 | alias: 'o', 16 | description: 'Set the output format', 17 | choices: ['json'], 18 | type: 'string', 19 | }, 20 | details: { 21 | alias: 'd', 22 | description: 'Show more details', 23 | type: 'boolean', 24 | }, 25 | field: { 26 | alias: 'f', 27 | description: 'Return only the indicated field', 28 | type: 'string', 29 | }, 30 | 'show-token': { 31 | description: 'Show tokens (hidden by default)', 32 | dest: 'showToken', 33 | type: 'boolean', 34 | }, 35 | }, 36 | }, 37 | params: { 38 | 'profile': { 39 | description: 'Profile to inspect', 40 | type: 'string', 41 | }, 42 | }, 43 | }); 44 | 45 | 46 | // Command handler 47 | 48 | function handleProfileGet(args) { 49 | var config = new ConfigFile(); 50 | 51 | return config.getProfile(args.profile) 52 | .then(function (profile) { 53 | if (args.field) { 54 | var value = profile[args.field.toLowerCase()]; 55 | 56 | if (!value) { 57 | throw Cli.error.invalid('Field `' + args.field + '` does not ' 58 | + 'exist'); 59 | } 60 | 61 | console.log(args.output === 'json' ? JSON.stringify(value) : value); 62 | } else { 63 | if (args.output === 'json') { 64 | console.log(profile); 65 | } else { 66 | 67 | PrintProfile(profile, { details: args.details, token: args.showToken }); 68 | 69 | if (!args.showToken) console.log(Chalk.bold('* Hint: Use --show-token to show the token for this profile.')); 70 | else console.log(Chalk.bold('* Warning: Tokens are like passwords and should not be shared.')); 71 | } 72 | } 73 | }); 74 | } 75 | 76 | -------------------------------------------------------------------------------- /bin/profile/init.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Chalk = require('chalk'); 3 | var Cli = require('structured-cli'); 4 | var ConfigFile = require('../../lib/config'); 5 | var PrintProfile = require('../../lib/printProfile'); 6 | var Promptly = Bluebird.promisifyAll(require('promptly')); 7 | var Sandbox = require('sandboxjs'); 8 | var UserVerifier = require('../../lib/userVerifier'); 9 | var UserAuthenticator = require('../../lib/userAuthenticator'); 10 | var _ = require('lodash'); 11 | 12 | 13 | module.exports = Cli.createCommand('init', { 14 | description: 'Create and update webtask profiles', 15 | plugins: [ 16 | require('../_plugins/profileOptions'), 17 | ], 18 | options: { 19 | 'admin': { 20 | description: 'Request admin permissions', 21 | dest: 'admin', 22 | type: 'boolean', 23 | }, 24 | 'auth0': { 25 | description: 'Intialize Auth0 account profile', 26 | dest: 'auth0', 27 | type: 'boolean', 28 | }, 29 | }, 30 | params: { 31 | 'email_or_phone': { 32 | description: 'Email or phone number that will be used to configure a new webtask profile.', 33 | type: 'string', 34 | }, 35 | }, 36 | handler: handleProfileInit, 37 | }); 38 | 39 | 40 | // Command handler 41 | 42 | function handleProfileInit(args) { 43 | 44 | var config = new ConfigFile(); 45 | 46 | return config.getProfile(args.profile) 47 | .then(confirmProfileOverwrite) 48 | // Ignore `E_NOTFOUND` errors which indicate that the profile does 49 | // not exist. 50 | .catch(_.matchesProperty('code', 'E_NOTFOUND'), _.identity) 51 | .then(verifyUserOrReturnProfile) 52 | .tap(updateProfile) 53 | .tap(showCompleteMessage); 54 | 55 | 56 | function confirmProfileOverwrite(profile) { 57 | console.log('You already have the `' + profile.name 58 | + '` profile:'); 59 | 60 | PrintProfile(profile); 61 | 62 | return Promptly.confirmAsync('Do you want to override it? [y/N]', { 63 | 'default': false, 64 | }) 65 | .then(function (override) { 66 | if (!override) { 67 | throw Cli.error.cancelled('Cancelled', profile); 68 | } 69 | }); 70 | } 71 | 72 | function verifyUserOrReturnProfile() { 73 | return (args.token && args.container && args.url) 74 | ? Sandbox.init(args) 75 | : detectAuthMode(args); 76 | } 77 | 78 | function updateProfile(profile) { 79 | return config.setProfile(args.profile, { 80 | url: profile.url, 81 | token: profile.token, 82 | container: profile.container, 83 | openid: profile.openid, 84 | }) 85 | .tap(function () { 86 | return config.save(); 87 | }); 88 | } 89 | 90 | function showCompleteMessage(profile) { 91 | console.log(Chalk.green('Welcome to webtasks! Create your first one as follows:\n\n' 92 | + Chalk.bold('$ echo "module.exports = function (cb) { cb(null, \'Hello\'); }" > hello.js\n') 93 | + Chalk.bold('$ wt create hello.js\n'))); 94 | } 95 | } 96 | 97 | 98 | // Private helper functions 99 | 100 | function detectAuthMode(args) { 101 | var url = args.url ? args.url : 'https://sandbox.auth0-extend.com'; 102 | return UserAuthenticator.create(url, args.auth0) 103 | .then(userAuthenticator => { 104 | if (!userAuthenticator) { 105 | if (args.admin) { 106 | throw Cli.error.invalid('Server does not support --admin flag.'); 107 | } 108 | return getVerifiedProfile(args); 109 | } 110 | else if (args.auth0 && !args.container) { 111 | throw Cli.error.invalid('When --auth0 is specified, the --container must also be provided.'); 112 | } 113 | return userAuthenticator.login({ auth0: args.auth0, container: args.container, admin: args.admin }); 114 | }) 115 | .catch(error => { 116 | var message = `Initialization Failed. Error: ${error.message}`; 117 | if (message.indexOf('access_denied')) { 118 | message = args.auth0 ? 119 | 'The given subscription does not support the Webtask CLI.' : 120 | 'Initialization failed due to invalid credentials.' 121 | } 122 | 123 | 124 | throw Cli.error.invalid(message); 125 | }); 126 | } 127 | 128 | function getVerifiedProfile (args) { 129 | var profile$ = args.email_or_phone 130 | ? sendVerificationCode(args.email_or_phone) 131 | : promptForEmailOrPhone(); 132 | 133 | 134 | return profile$ 135 | .catch(_.matchesProperty('code', 'E_INVALID'), function (err) { 136 | console.log(Chalk.red(err.message)); 137 | 138 | return promptForEmailOrPhone(); 139 | }) 140 | .then(function (profile) { 141 | return Sandbox.init({ 142 | url: profile.url, 143 | token: profile.token, 144 | container: profile.tenant, 145 | }); 146 | }) 147 | .catch(function (err) { 148 | console.log(Chalk.red('We were unable to verify your identity.')); 149 | 150 | return Promptly.confirmAsync('Would you like to try again? [Y/n]', { 151 | 'default': true, 152 | }) 153 | .then(function (tryAgain) { 154 | if (!tryAgain) { 155 | throw Cli.error.cancelled('Failed to verify identity', err); 156 | } 157 | 158 | return getVerifiedProfile(args); 159 | }); 160 | }); 161 | 162 | 163 | function promptForEmailOrPhone() { 164 | console.log('Please enter your e-mail or phone number, we will send you a verification code.'); 165 | 166 | return Promptly.promptAsync('E-mail or phone number:') 167 | .then(sendVerificationCode); 168 | } 169 | 170 | function sendVerificationCode (phoneOrEmail) { 171 | var verifier = new UserVerifier({}); 172 | var FIVE_MINUTES = 1000 * 60 * 5; 173 | 174 | return verifier.requestVerificationCode(phoneOrEmail) 175 | .then(promptForVerificationCode); 176 | 177 | 178 | function promptForVerificationCode(verifyFunc) { 179 | console.log('Please enter the verification code we sent to ' 180 | + phoneOrEmail + ' below.'); 181 | 182 | return Promptly.promptAsync('Verification code:') 183 | .then(verifyFunc) 184 | .timeout(FIVE_MINUTES, 'Verification code expired') 185 | .catch(function (e) { 186 | console.log('\n' + Chalk.red(e.message) + '\n'); 187 | }); 188 | } 189 | } 190 | } 191 | 192 | module.exports.handleProfileInit = handleProfileInit; 193 | -------------------------------------------------------------------------------- /bin/profile/ls.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var ConfigFile = require('../../lib/config'); 4 | var PrintProfile = require('../../lib/printProfile'); 5 | var _ = require('lodash'); 6 | 7 | 8 | module.exports = Cli.createCommand('ls', { 9 | description: 'List existing webtask profiles', 10 | optionGroups: { 11 | 'Output options': { 12 | 'output': { 13 | alias: 'o', 14 | description: 'Set the output format', 15 | choices: ['json'], 16 | type: 'string', 17 | }, 18 | 'details': { 19 | alias: 'd', 20 | description: 'Show more details', 21 | type: 'boolean', 22 | }, 23 | 'show-token': { 24 | description: 'Show tokens (hidden by default)', 25 | dest: 'showToken', 26 | type: 'boolean', 27 | }, 28 | }, 29 | }, 30 | handler: handleProfileList, 31 | }); 32 | 33 | 34 | // Command handler 35 | 36 | function handleProfileList(args) { 37 | var config = new ConfigFile(); 38 | 39 | return config.load() 40 | .tap(function (profiles) { 41 | if (args.output === 'json') { 42 | var props = ['url', 'container']; 43 | 44 | // Strip tokens by default 45 | if (args.showToken) { 46 | props.push('token'); 47 | } 48 | 49 | console.log(_.mapValues(profiles, _.partialRight(_.pick, props))); 50 | } else if (_.isEmpty(profiles)) { 51 | throw Cli.error.hint('No webtask profiles found. To get started:\n' 52 | + Chalk.bold('$ wt init')); 53 | } 54 | else { 55 | var i = 0; 56 | _.forEach(profiles, function (profile, profileName) { 57 | if (i++) console.log(); 58 | PrintProfile(profile, { details: args.details, token: args.showToken }); 59 | }); 60 | 61 | if (!args.showToken) console.log(Chalk.bold('* Hint: Use --show-token to show the token for these profiles.')); 62 | else console.log(Chalk.bold('* Warning: Tokens are like passwords and should not be shared.')); 63 | 64 | } 65 | }); 66 | } 67 | 68 | -------------------------------------------------------------------------------- /bin/profile/nuke.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Chalk = require('chalk'); 3 | var Cli = require('structured-cli'); 4 | var ConfigFile = require('../../lib/config'); 5 | var Promptly = Bluebird.promisifyAll(require('promptly')); 6 | var _ = require('lodash'); 7 | 8 | 9 | module.exports = Cli.createCommand('nuke', { 10 | description: 'Destroy all existing profiles and their secrets', 11 | handler: handleProfileNuke, 12 | options: { 13 | force: { 14 | alias: 'f', 15 | description: 'Do not prompt for confirmation', 16 | type: 'boolean', 17 | }, 18 | silent: { 19 | alias: 's', 20 | description: 'No output', 21 | type: 'boolean', 22 | }, 23 | }, 24 | }); 25 | 26 | 27 | // Command handler 28 | 29 | function handleProfileNuke(args) { 30 | var config = new ConfigFile(); 31 | 32 | return config.load() 33 | .then(function () { 34 | return args.force 35 | ? true 36 | : Promptly.confirmAsync('Do you want to remove all secrets and ' 37 | + 'profile information?? [yN]', { 38 | 'default': false, 39 | }) 40 | .then(function (force) { 41 | if (!force) 42 | throw Cli.error.cancelled('Cancelled'); 43 | }); 44 | }) 45 | .then(config.removeAllProfiles.bind(config)) 46 | .then(config.save.bind(config)) 47 | .then(function () { 48 | if (!args.silent) { 49 | console.log(Chalk.green('All secrets and profiles deleted. Initialize ' 50 | + 'again with `wt init`.')); 51 | } 52 | }); 53 | } 54 | 55 | -------------------------------------------------------------------------------- /bin/profile/rm.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var ConfigFile = require('../../lib/config'); 4 | var _ = require('lodash'); 5 | 6 | 7 | module.exports = Cli.createCommand('rm', { 8 | description: 'Remove a saved webtask profile', 9 | options: { 10 | silent: { 11 | alias: 's', 12 | description: 'No output', 13 | type: 'boolean', 14 | }, 15 | }, 16 | params: { 17 | 'profile': { 18 | description: 'Profile to remove', 19 | type: 'string', 20 | defaultValue: 'default', 21 | }, 22 | }, 23 | handler: handleProfileRemove, 24 | }); 25 | 26 | 27 | // Command handler 28 | 29 | function handleProfileRemove(args) { 30 | var config = new ConfigFile(); 31 | 32 | 33 | return config.removeProfile(args.profile) 34 | .then(config.save.bind(config)) 35 | .then(function () { 36 | if (!args.silent) { 37 | console.log(Chalk.green('Profile `' + args.profile + '` removed.')); 38 | } 39 | }); 40 | } 41 | 42 | -------------------------------------------------------------------------------- /bin/rm.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | 4 | 5 | module.exports = Cli.createCommand('rm', { 6 | description: 'Remove a named webtask', 7 | plugins: [ 8 | require('./_plugins/profile'), 9 | ], 10 | options: { 11 | silent: { 12 | alias: 's', 13 | description: 'No output', 14 | type: 'boolean', 15 | }, 16 | }, 17 | params: { 18 | 'name': { 19 | description: 'Webtask name', 20 | type: 'string', 21 | required: true, 22 | }, 23 | }, 24 | handler: handleWebtaskRemove, 25 | }); 26 | 27 | 28 | // Command handler 29 | 30 | function handleWebtaskRemove(args) { 31 | var profile = args.profile; 32 | 33 | 34 | return profile.removeWebtask({ name: args.name }) 35 | .then(onSuccess, onError); 36 | 37 | function onSuccess() { 38 | if (!args.silent) { 39 | console.log(Chalk.green('Removed webtask: %s'), Chalk.bold(args.name)); 40 | } 41 | } 42 | 43 | function onError(err) { 44 | switch (err.statusCode) { 45 | case 404: throw Cli.error.notFound('No such webtask: ' + Chalk.bold(args.name)); 46 | default: throw err; 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /bin/serve.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Bluebird = require('bluebird'); 4 | const Chalk = require('chalk'); 5 | const Cli = require('structured-cli'); 6 | const Dotenv = require('dotenv'); 7 | const Fs = require('fs'); 8 | const keyValList2Object = require('../lib/keyValList2Object'); 9 | const Path = require('path'); 10 | const Runtime = require('webtask-runtime'); 11 | const _ = require('lodash'); 12 | const config = require('./serveCommon')(); 13 | 14 | config.description = "Run a webtask as a local http server"; 15 | config.handler = handleWebtaskServe; 16 | 17 | module.exports = Cli.createCommand('serve', config); 18 | 19 | // Command handler 20 | 21 | function handleWebtaskServe(args) { 22 | keyValList2Object(args, 'secrets'); 23 | keyValList2Object(args, 'params'); 24 | keyValList2Object(args, 'meta'); 25 | 26 | if (args.secretsFile) { 27 | try { 28 | const filename = Path.resolve(process.cwd(), args.secretsFile); 29 | const content = Fs.readFileSync(filename, 'utf8'); 30 | const secrets = Dotenv.parse(content); 31 | 32 | for (let secret in secrets) { 33 | if (!args.secrets.hasOwnProperty(secret)) { 34 | args.secrets[secret] = secrets[secret]; 35 | } 36 | } 37 | } catch (e) { 38 | throw Cli.error.invalid(`Error loading secrets file: ${e.message}`); 39 | } 40 | } 41 | 42 | if (args.metaFile) { 43 | try { 44 | const filename = Path.resolve(process.cwd(), args.metaFile); 45 | const content = Fs.readFileSync(filename, 'utf8'); 46 | const meta = Dotenv.parse(content); 47 | 48 | for (let key in meta) { 49 | if (!args.meta.hasOwnProperty(key)) { 50 | args.meta[key] = meta[key]; 51 | } 52 | } 53 | } catch (e) { 54 | throw Cli.error.invalid(`Error loading meta file: ${e.message}`); 55 | } 56 | } 57 | return Bluebird.using(createServer(), server => { 58 | return server.listenAsync(args.port, args.hostname) 59 | .tap(() => { 60 | const address = server.address(); 61 | 62 | console.log('Your webtask is now listening for %s traffic on %s:%s', Chalk.green(address.family), Chalk.green.bold(address.address), Chalk.green.bold(address.port)); 63 | }) 64 | .delay(1000 * 60 * 30) 65 | .then(() => { 66 | console.log('Automatically shutting down your webtask server after 30m'); 67 | }); 68 | }).timeout(1000 * 60 * 30); 69 | 70 | function createServer() { 71 | const promise$ = new Bluebird((resolve, reject) => { 72 | try { 73 | const webtask = require(Path.resolve(process.cwd(), args.filename)); 74 | const server = Runtime.createServer(webtask, { 75 | parseBody: args.parseBody ? Runtime.PARSE_ALWAYS : undefined, 76 | mergeBody: args.mergeBody, 77 | secrets: args.secrets, 78 | meta: args.meta, 79 | params: args.params, 80 | shortcutFavicon: true, 81 | storageFile: args.storageFile, 82 | }); 83 | 84 | return resolve(Bluebird.promisifyAll(server)); 85 | } catch (e) { 86 | return reject(new Error(`Error starting local server: ${e.message}`)); 87 | } 88 | }); 89 | 90 | return promise$ 91 | .disposer(server => { 92 | server.listening 93 | ? server.closeAsync() 94 | .tap(() => console.log('Webtask server shut down')) 95 | : Bluebird.resolve(); 96 | }); 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /bin/serveCommon.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return { 3 | optionGroups: { 4 | 'Server options': { 5 | port: { 6 | alias: 'p', 7 | description: 'Port on which the webtask server will listen', 8 | type: 'int', 9 | defaultValue: 8080, 10 | }, 11 | 'hostname': { 12 | description: 'The hostname for the http listener', 13 | type: 'string', 14 | defaultValue: '127.0.0.1', 15 | }, 16 | }, 17 | 'Webtask creation': { 18 | 'secret': { 19 | action: 'append', 20 | alias: 's', 21 | defaultValue: [], 22 | description: 'Secret(s) exposed to your code as `secrets` on the webtask context object. These secrets will be encrypted and stored in a webtask token in such a way that only the webtask server is able to decrypt the secrets so that they may be exposed to your running webtask code.', 23 | dest: 'secrets', 24 | metavar: 'KEY=VALUE', 25 | type: 'string', 26 | }, 27 | 'secrets-file': { 28 | description: 'A file containing one secret per line in KEY=VALUE format', 29 | dest: 'secretsFile', 30 | metavar: 'FILENAME', 31 | type: 'string', 32 | }, 33 | 'meta': { 34 | action: 'append', 35 | alias: 'm', 36 | defaultValue: [], 37 | description: 'Metadata describing the webtask. This is a set of string key value pairs.', 38 | dest: 'meta', 39 | metavar: 'KEY=VALUE', 40 | type: 'string', 41 | }, 42 | 'meta-file': { 43 | description: 'A file containing one meta per line in KEY=VALUE format', 44 | dest: 'metaFile', 45 | metavar: 'FILENAME', 46 | type: 'string', 47 | }, 48 | 'param': { 49 | action: 'append', 50 | defaultValue: [], 51 | description: 'Param(s) exposed to your code as `params` on the webtask context object. The properties will be signed and protected from interference but not encrypted.', 52 | dest: 'params', 53 | metavar: 'KEY=VALUE', 54 | type: 'string', 55 | }, 56 | 'no-merge': { 57 | action: 'storeFalse', 58 | defaultValue: true, 59 | description: 'Disable automatic merging of the parsed body and secrets into the `data` field of the webtask context object. The parsed body (if available) will be on the `body` field and secrets on the `secrets` field.', 60 | dest: 'mergeBody', 61 | }, 62 | 'no-parse': { 63 | description: 'Deprecated and ignored.' 64 | }, 65 | 'parse-body': { 66 | descrption: 'Automatically parse JSON and application/x-www-form-urlencoded request bodies. Use this with (ctx, req, res) webtask signatures if you want webtask runtime to parse the request body and store it in ctx.body.', 67 | type: 'boolean', 68 | dest: 'parseBody' 69 | }, 70 | 'storage-file': { 71 | description: 'Provide a file that will be used to initialize and persist webtask storage data', 72 | dest: 'storageFile', 73 | }, 74 | }, 75 | }, 76 | params: { 77 | 'filename': { 78 | description: 'The path to the webtask\'s source code', 79 | type: 'string', 80 | required: true, 81 | }, 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bin/token.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | 3 | 4 | var category = module.exports = Cli.createCategory('token', { 5 | description: 'Advanced operations on webtask tokens', 6 | }); 7 | 8 | category.addChild(require('./token/create')); 9 | category.addChild(require('./token/inspect')); 10 | category.addChild(require('./token/revoke')); 11 | -------------------------------------------------------------------------------- /bin/token/create.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | var _ = require('lodash'); 4 | 5 | 6 | var RAW_CLAIMS = { 7 | ten: { 8 | type: 'string', 9 | }, 10 | dd: { 11 | type: 'int', 12 | }, 13 | 'code-url': { 14 | type: 'string', 15 | }, 16 | code: { 17 | type: 'string', 18 | description: '', 19 | }, 20 | ectx: { 21 | action: 'append', 22 | defaultValue: null, 23 | metavar: 'KEY=VALUE', 24 | type: 'string', 25 | }, 26 | pctx: { 27 | action: 'append', 28 | defaultValue: null, 29 | metavar: 'KEY=VALUE', 30 | type: 'string', 31 | }, 32 | nbf: { 33 | type: 'string', 34 | }, 35 | exp: { 36 | type: 'string', 37 | }, 38 | host: { 39 | type: 'string' 40 | }, 41 | mb: { 42 | type: 'boolean', 43 | }, 44 | pb: { 45 | type: 'int', 46 | }, 47 | dr: { 48 | type: 'boolean', 49 | }, 50 | jtn: { 51 | type: 'string', 52 | }, 53 | ls: { 54 | type: 'string', 55 | }, 56 | lm: { 57 | type: 'string', 58 | }, 59 | lh: { 60 | type: 'string', 61 | }, 62 | ld: { 63 | type: 'string', 64 | }, 65 | lw: { 66 | type: 'string', 67 | }, 68 | lo: { 69 | type: 'string', 70 | }, 71 | lts: { 72 | type: 'string', 73 | }, 74 | ltm: { 75 | type: 'string', 76 | }, 77 | lth: { 78 | type: 'string', 79 | }, 80 | ltd: { 81 | type: 'string', 82 | }, 83 | ltw: { 84 | type: 'string', 85 | }, 86 | lto: { 87 | type: 'string', 88 | }, 89 | }; 90 | 91 | module.exports = Cli.createCommand('create', { 92 | description: 'Create webtask tokens', 93 | plugins: [ 94 | require('../_plugins/profile'), 95 | ], 96 | optionGroups: { 97 | 'Token claims': RAW_CLAIMS, 98 | 'Output options': { 99 | output: { 100 | alias: 'o', 101 | description: 'Set the output format', 102 | choices: ['json'], 103 | type: 'string', 104 | }, 105 | }, 106 | }, 107 | options: { 108 | claims: { 109 | type: 'string', 110 | description: 'Provide your own base set of claims as a JSON string' 111 | }, 112 | }, 113 | epilog: Chalk.bold('For detailed information on creating webtask tokens, see: https://webtask.io/docs/api_issue'), 114 | handler: handleTokenCreate, 115 | }); 116 | 117 | 118 | // Command handler 119 | 120 | function handleTokenCreate(args) { 121 | var profile = args.profile; 122 | 123 | if (profile.securityVersion !== 'v1') { 124 | throw Cli.error.invalid('The `wt token create` command is not supported by the target service security configuration.'); 125 | } 126 | 127 | var claims = _.pick(_.pickBy(args, v => v !== null), Object.keys(RAW_CLAIMS)); 128 | 129 | if (claims['code-url']) { 130 | claims.url = claims['code-url']; 131 | delete claims['code-url']; 132 | } 133 | 134 | if (claims.dr) claims.dr = 1; 135 | else delete claims.dr; 136 | 137 | if (claims.mb) claims.mb = 1; 138 | else delete claims.mb; 139 | 140 | if (args.claims) { 141 | try { 142 | claims = _.defaultsDeep(claims, JSON.parse(args.claims)); 143 | } catch (e) { 144 | throw Cli.error.invalid('Invalid JSON supplied in `claims` option'); 145 | } 146 | } 147 | 148 | if (claims.ectx) claims.ectx = parseTuples(claims.ectx); 149 | if (claims.pctx) claims.pctx = parseTuples(claims.pctx); 150 | 151 | return profile.createTokenRaw(claims) 152 | .catch(function (err) { 153 | if (err.statusCode >= 500) throw Cli.error.serverError(err.message); 154 | if (err.statusCode >= 400) throw Cli.error.badRequest(err.message); 155 | 156 | throw err; 157 | }) 158 | .then(onToken); 159 | 160 | 161 | function onToken(token) { 162 | console.log(args.output === 'json' ? JSON.stringify(token) : token); 163 | } 164 | 165 | function parseTuples(tuples) { 166 | return _(tuples) 167 | .map(_.method('split', /,\s*/)) 168 | .flatten() 169 | .reduce(function (secrets, tuple) { 170 | var parts = tuple.split('='); 171 | 172 | return _.set(secrets, parts.shift(), parts.join('=')); 173 | }, {}); 174 | } 175 | } -------------------------------------------------------------------------------- /bin/token/inspect.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | var Decode = require('jwt-decode'); 3 | var PrintTokenDetails = require('../../lib/printTokenDetails'); 4 | var PrintWebtaskDetails = require('../../lib/printWebtaskDetails'); 5 | 6 | 7 | module.exports = Cli.createCommand('inspect', { 8 | description: 'Inspect named webtasks and webtask tokens', 9 | plugins: [ 10 | require('../_plugins/profile'), 11 | ], 12 | handler: handleTokenInspect, 13 | optionGroups: { 14 | 'Inspect options': { 15 | 'decrypt': { 16 | type: 'boolean', 17 | description: 'Return the decrypted secrets', 18 | }, 19 | 'fetch-code': { 20 | type: 'boolean', 21 | description: 'Return the webtask code', 22 | dest: 'fetchCode', 23 | }, 24 | }, 25 | 'Output options': { 26 | output: { 27 | alias: 'o', 28 | description: 'Set the output format', 29 | choices: ['json'], 30 | type: 'string', 31 | }, 32 | }, 33 | }, 34 | params: { 35 | subject: { 36 | description: 'The subject token or webtask name to be inspected', 37 | type: 'string', 38 | required: true, 39 | }, 40 | }, 41 | }); 42 | 43 | 44 | // Command handler 45 | 46 | function handleTokenInspect(args) { 47 | var profile = args.profile; 48 | var claims; 49 | 50 | try { 51 | claims = Decode(args.subject); 52 | } catch (__) { } 53 | 54 | if (claims && profile.securityVersion !== 'v1') { 55 | throw Cli.error.invalid('The `wt token inspect` command for webtask tokens is not supported by the target service security configuration.'); 56 | } 57 | 58 | var inspection$ = claims 59 | ? profile.inspectToken({ token: args.subject, decrypt: args.decrypt, fetch_code: args.fetchCode, meta: +!!claims.jtn }) 60 | : profile.inspectWebtask({ name: args.subject, decrypt: args.decrypt, fetch_code: args.fetchCode, meta: 1 }); 61 | 62 | return inspection$ 63 | .catch(function (err) { 64 | if (err.statusCode >= 500) throw Cli.error.serverError(err.message); 65 | if (err.statusCode >= 400) throw Cli.error.badRequest(err.message); 66 | 67 | throw err; 68 | }) 69 | .then(onTokenData); 70 | 71 | 72 | function onTokenData(data) { 73 | if (args.output === 'json') { 74 | console.log(JSON.stringify(data, null, 2)); 75 | } else if (profile.securityVersion === 'v1') { 76 | PrintTokenDetails(data); 77 | } 78 | else { 79 | PrintWebtaskDetails(data); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /bin/token/revoke.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Cli = require('structured-cli'); 3 | 4 | 5 | module.exports = Cli.createCommand('revoke', { 6 | description: 'Revoke a webtask token', 7 | plugins: [ 8 | require('../_plugins/profile'), 9 | ], 10 | optionGroups: { 11 | 'Output options': { 12 | output: { 13 | alias: 'o', 14 | description: 'Set the output format', 15 | choices: ['json'], 16 | type: 'string', 17 | }, 18 | }, 19 | }, 20 | handler: handleTokenRevoke, 21 | params: { 22 | subject: { 23 | description: 'The subject token to be revoked', 24 | type: 'string', 25 | required: true, 26 | }, 27 | }, 28 | }); 29 | 30 | 31 | // Command handler 32 | 33 | function handleTokenRevoke(args) { 34 | var profile = args.profile; 35 | 36 | if (profile.securityVersion !== 'v1') { 37 | throw Cli.error.invalid('The `wt token revoke` command is not supported by the target service security configuration.'); 38 | } 39 | 40 | return profile.revokeToken(args.subject) 41 | .catch(function (err) { 42 | if (err.statusCode >= 500) throw Cli.error.serverError(err.message); 43 | if (err.statusCode >= 400) throw Cli.error.badRequest(err.message); 44 | 45 | throw err; 46 | }) 47 | .then(onTokenData); 48 | 49 | 50 | function onTokenData(data) { 51 | if (args.output === 'json') { 52 | console.log(true); 53 | } else { 54 | console.log(Chalk.green('Token revoked.')); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /bin/update.js: -------------------------------------------------------------------------------- 1 | var Cli = require('structured-cli'); 2 | var CreateWebtask = require('../lib/createWebtask'); 3 | var ValidateCreateArgs = require('../lib/validateCreateArgs'); 4 | 5 | 6 | module.exports = Cli.createCommand('update', { 7 | description: 'Update the code of a named webtask', 8 | plugins: [ 9 | require('./_plugins/profile'), 10 | ], 11 | optionGroups: { 12 | 'Output options': { 13 | output: { 14 | alias: 'o', 15 | description: 'Set the output format', 16 | choices: ['json'], 17 | type: 'string', 18 | }, 19 | 'show-token': { 20 | description: 'Show tokens (hidden by default)', 21 | dest: 'showToken', 22 | type: 'boolean', 23 | }, 24 | }, 25 | 'Webtask creation': { 26 | 'dependency': { 27 | action: 'append', 28 | alias: 'd', 29 | defaultValue: [], 30 | description: 'Specify a dependency on a node module. The best matching version of this node module (at the time of webtask creation) will be available in your webtask code via `require()`. You can use this option more than once to add mutliple dependencies.', 31 | dest: 'dependencies', 32 | metavar: 'NAME@VERSION', 33 | type: 'string', 34 | }, 35 | 'ignore-package-json': { 36 | description: 'Ignore any dependencies found in a package.json file adjacent to your webtask.', 37 | dest: 'ignorePackageJson', 38 | type: 'boolean', 39 | }, 40 | 'watch': { 41 | alias: 'w', 42 | description: 'Automatically watch and reprovision the webtask on local file changes. This will also subscribe you to logs as if you had done `wt logs` to provide an intuitive development experience without requiring multiple active terminals.', 43 | type: 'boolean', 44 | }, 45 | 'bundle': { 46 | alias: 'b', 47 | description: 'Use `webtask-bundle` to bundle your code into a single file. This tool can compile ES2015 (ES6) code via Babel as well as packaging up a webtask composed of multiple files into a single file. The tool will scan your package.json for dependencies and will automatically bundle those that are not available on the webtask platform. Enabling --bundle-loose will prevent this check from doing strict semver range comparisons on dependencies.', 48 | type: 'boolean', 49 | }, 50 | 'bundle-minify': { 51 | description: 'Generate a minified production build', 52 | type: 'boolean', 53 | dest: 'minify' 54 | }, 55 | 'bundle-strict': { 56 | description: 'Enforce strict semver matching for bundling with `webtask-bundle`', 57 | dest: 'loose', 58 | action: 'storeFalse', 59 | defaultValue: true, 60 | type: 'boolean', 61 | }, 62 | 'capture': { 63 | description: 'Download and use the current code indicated by `url`. When you are developing a webtask whose code is remotely hosted, this option will automatically download the remote code before creating the webtask. This means that the webtask will continue to run even if the remote url becomes unavailable.', 64 | type: 'boolean', 65 | }, 66 | }, 67 | }, 68 | params: { 69 | 'name': { 70 | description: 'The name of the webtask to update.', 71 | type: 'string', 72 | required: true, 73 | }, 74 | 'file_or_url': { 75 | description: 'Path or URL of the webtask\'s code. When not specified, code will be read from STDIN.', 76 | type: 'string', 77 | }, 78 | }, 79 | handler: handleUpdate, 80 | }); 81 | 82 | 83 | // Command handler 84 | 85 | function handleUpdate(args) { 86 | args = ValidateCreateArgs(args); 87 | 88 | var profile = args.profile; 89 | 90 | return profile.inspectWebtask({ name: args.name, decrypt: true, meta: true }) 91 | .then(onClaims); 92 | 93 | 94 | function onClaims(claims) { 95 | // Set the user-defined options from the inspected webtask's claims 96 | args.host = claims.host; 97 | args.merge = claims.mb; 98 | args.parse = claims.pb; 99 | args.secrets = claims.ectx; 100 | args.params = claims.pctx; 101 | args.meta = claims.meta; 102 | 103 | // Defer to the functionality of the create command 104 | return CreateWebtask(args, { action: 'updated' }); 105 | } 106 | } 107 | 108 | 109 | -------------------------------------------------------------------------------- /bin/wt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Chalk = require('chalk'); 3 | var Cli = require('structured-cli'); 4 | var Package = require('../package.json'); 5 | var Updates = require('update-notifier'); 6 | var _ = require('lodash'); 7 | 8 | 9 | var notifier = Updates({ 10 | pkg: { 11 | name: Package.name, 12 | version: Package.version, 13 | } 14 | }); 15 | 16 | notifier.notify(); 17 | 18 | 19 | var cli = Cli.createApp({ 20 | description: Package.description, 21 | version: Package.version, 22 | epilog: Chalk.underline('Sample usage:') + '\n' 23 | + '1. Set up a webtask profile:' + '\n' 24 | + Chalk.bold(' $ wt init') + '\n' 25 | + '2. Create a basic webtask:' + '\n' 26 | + Chalk.bold(' $ wt create ./sample-webtasks/hello-world.js') + '\n' 27 | }); 28 | 29 | 30 | cli.addChild(require('./init')); 31 | cli.addChild(require('./create')); 32 | cli.addChild(require('./ls')); 33 | cli.addChild(require('./rm')); 34 | cli.addChild(require('./serve')); 35 | cli.addChild(require('./debug')); 36 | cli.addChild(require('./edit')); 37 | cli.addChild(require('./browse')); 38 | cli.addChild(require('./inspect')); 39 | cli.addChild(require('./update')); 40 | cli.addChild(require('./profile')); 41 | cli.addChild(require('./cron')); 42 | cli.addChild(require('./logs')); 43 | cli.addChild(require('./token')); 44 | cli.addChild(require('./modules')); 45 | 46 | 47 | Cli.run(cli) 48 | .timeout(1000 * 60 * 30, Cli.error.timeout('Command timed out after 30 min')) 49 | // Code: 1 50 | .catch(_.matchesProperty('code', 'E_CANCELLED'), function (err) { 51 | console.error(err.message); 52 | 53 | process.exit(1); 54 | }) 55 | // Code: 2 56 | .catch(_.matchesProperty('code', 'E_INVALID'), function (err) { 57 | console.error(err.parser.formatUsage()); 58 | console.error(Chalk.red(err.message)); 59 | 60 | process.exit(2); 61 | }) 62 | // Code: 3 63 | .catch(_.matchesProperty('code', 'E_HINT'), function (err) { 64 | console.error(err.message); 65 | 66 | process.exit(3); 67 | }) 68 | // Code: 4 69 | .catch(_.matchesProperty('code', 'E_TIMEOUT'), function (err) { 70 | console.error(Chalk.red(err.message)); 71 | 72 | process.exit(4); 73 | }) 74 | // Code: 5 75 | .catch(_.matchesProperty('code', 'E_NOTFOUND'), function (err) { 76 | console.error(Chalk.red(err.message)); 77 | 78 | process.exit(5); 79 | }) 80 | // Code: 6 81 | .catch(_.matchesProperty('code', 'E_BADREQUEST'), function (err) { 82 | console.error(Chalk.red(err.message)); 83 | 84 | process.exit(6); 85 | }) 86 | // Code: 7 87 | .catch(_.matchesProperty('code', 'E_SERVERERROR'), function (err) { 88 | console.error(Chalk.red(err.message)); 89 | 90 | process.exit(7); 91 | }) 92 | // Code: 8 93 | .catch(_.matchesProperty('code', 'E_NOTAUTHORIZED'), function (err) { 94 | console.error(Chalk.red(err.message)); 95 | 96 | process.exit(8); 97 | }) // Code: 99 98 | .catch(function (err) { 99 | console.error(Chalk.red('Uncaught error: ', err.message)); 100 | console.error(err.stack); 101 | console.error('Please report this at: https://github.com/auth0/wt-cli/issues'); 102 | 103 | process.exit(99); 104 | }) 105 | .then(function () { 106 | process.exit(0); 107 | }); 108 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Cli = require('structured-cli'); 3 | var Fs = Bluebird.promisifyAll(require('fs')); 4 | var Path = require('path'); 5 | var Sandbox = require('sandboxjs'); 6 | var SuperagentProxy = require('superagent-proxy'); 7 | var _ = require('lodash'); 8 | 9 | 10 | module.exports = ConfigFile; 11 | 12 | 13 | function ConfigFile (configPath) { 14 | if (!configPath) { 15 | var homePath = process.env[(process.platform == 'win32') 16 | ? 'USERPROFILE' 17 | : 'HOME' 18 | ]; 19 | configPath = Path.join(homePath, '.webtask'); 20 | } 21 | 22 | this.configPath = configPath; 23 | this.profiles = {}; 24 | this.loaded = null; 25 | } 26 | 27 | ConfigFile.prototype.load = function (cb) { 28 | var self = this; 29 | var readFile = Bluebird.promisify(Fs.readFile, Fs); 30 | 31 | if (!this.loaded) { 32 | this.loaded = readFile(this.configPath, 'utf8') 33 | .catch(function (e) { 34 | if (e.code === 'ENOENT') return '{}'; 35 | else throw e; 36 | }) 37 | .then(JSON.parse) 38 | .then(function (profiles) { 39 | self.profiles = _.mapValues(profiles, function (profileData, profileName) { 40 | profileData.onBeforeRequest = onBeforeRequest; 41 | var profile = Sandbox.init(profileData); 42 | 43 | profile.name = profileName; 44 | profile.openid = profileData.openid; 45 | 46 | return profile; 47 | }); 48 | 49 | return self.profiles; 50 | }); 51 | } 52 | 53 | return cb ? this.loaded.nodeify(cb) : this.loaded; 54 | }; 55 | 56 | ConfigFile.prototype.save = function (cb) { 57 | var data = _.mapValues(this.profiles, _.partialRight(_.pick, ['url', 'token', 'container', 'openid'])); 58 | var profileData = JSON.stringify(data, null, 2); 59 | 60 | var promise$ = Fs.writeFileAsync(this.configPath, profileData, { encoding: 'utf8', mode: 0o600 }); 61 | 62 | return cb ? promise$.nodeify(cb) : promise$; 63 | }; 64 | 65 | ConfigFile.prototype.getProfile = function (profileName, cb) { 66 | var promise = this.load() 67 | .then(function (profiles) { 68 | if (!profileName) { 69 | profileName = Object.keys(profiles).length === 1 70 | ? Object.keys(profiles)[0] 71 | : 'default'; 72 | } 73 | 74 | var profile = profiles[profileName]; 75 | 76 | if (!profile) 77 | throw new Cli.error.notFound('Profile `' + profileName 78 | + '` not found'); 79 | 80 | return profile; 81 | }); 82 | 83 | return cb ? promise.nodeify(cb) : promise; 84 | }; 85 | 86 | ConfigFile.prototype.setProfile = function (profileName, profileData, cb) { 87 | if (!profileName) { 88 | profileName = 'default'; 89 | } 90 | 91 | var promise = this.load() 92 | .then(function (profiles) { 93 | return (profiles[profileName] = profileData); 94 | }); 95 | 96 | return cb ? promise.nodeify(cb) : promise; 97 | }; 98 | 99 | ConfigFile.prototype.removeProfile = function (profileName, cb) { 100 | var promise = this.load() 101 | .then(function (profiles) { 102 | if (!profiles[profileName]) 103 | throw Cli.error.notFound('No such profile `' + profileName + '`'); 104 | 105 | delete profiles[profileName]; 106 | }); 107 | 108 | return cb ? promise.nodeify(cb) : promise; 109 | }; 110 | 111 | ConfigFile.prototype.removeAllProfiles = function (cb) { 112 | this.profiles = {}; 113 | 114 | var promise = this.save(); 115 | 116 | return cb ? promise.nodeify(cb) : promise; 117 | }; 118 | 119 | 120 | function onBeforeRequest(request) { 121 | const proxy = process.env.http_proxy || process.env.HTTP_PROXY; 122 | const result = proxy 123 | ? SuperagentProxy(request, proxy) 124 | : request; 125 | 126 | return result; 127 | } 128 | -------------------------------------------------------------------------------- /lib/createWebtask.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Chalk = require('chalk'); 4 | const Logs = require('../lib/logs'); 5 | const WebtaskCreator = require('../lib/webtaskCreator'); 6 | 7 | module.exports = createWebtask; 8 | 9 | function createWebtask(args, options) { 10 | if (!options) options = {}; 11 | if (!options.action) options.action = 'created'; 12 | 13 | const profile = args.profile; 14 | const logger = args.watch ? Logs.createLogStream(profile) : { 15 | info: function info() { return console.log.apply(console, Array.prototype.slice.call(arguments)); }, // eslint-disable-line no-console 16 | warn: function warn() { return console.log.apply(console, Array.prototype.slice.call(arguments)); }, // eslint-disable-line no-console 17 | error: function error() { return console.log.apply(console, Array.prototype.slice.call(arguments)); }, // eslint-disable-line no-console 18 | }; 19 | 20 | if (args.dependencies.length && args.packageJsonPath) { 21 | logger.info( 22 | Chalk.italic( 23 | `* Hint: Since you have specified dependencies using the ${Chalk.bold('--dependency')} flag, the dependencies from the ${Chalk.bold('package.json')} file adjacent to your webtask file will be ignored.` 24 | ) 25 | ); 26 | 27 | args.packageJsonPath = null; 28 | } 29 | 30 | if ( 31 | !args.dependencies.length && 32 | args.packageJsonPath && 33 | !args.ignorePackageJson 34 | ) { 35 | logger.info( 36 | Chalk.italic( 37 | `* Hint: A ${Chalk.bold('package.json')} file has been detected adjacent to your webtask. Ensuring that all ${Chalk.bold('dependencies')} from that file are available on the platform. This may take a few minutes for new versions of modules so please be patient.` 38 | ) 39 | ); 40 | logger.info( 41 | Chalk.italic( 42 | `* Hint: If you would like to opt-out from this behaviour, pass in the ${Chalk.bold('--ignore-package-json')} flag.` 43 | ) 44 | ); 45 | } 46 | 47 | const createWebtask = WebtaskCreator(args, { 48 | logger, 49 | onGeneration: onGeneration, 50 | onError: onError, 51 | }); 52 | 53 | return createWebtask(profile); 54 | 55 | function onError(build) { 56 | formatError(build); 57 | } 58 | 59 | function onGeneration(build) { 60 | formatOutput(build, build.webtask.url); 61 | } 62 | 63 | function formatError(build) { 64 | if (args.watch) { 65 | const output = { generation: build.generation }; 66 | 67 | if (build.stats.errors.length) { 68 | logger.error(output, `Bundling failed: ${ build.stats.errors[0] }`); 69 | } 70 | } else if (args.output === 'json') { 71 | const json = { 72 | name: args.name, 73 | container: args.profile.container, 74 | errors: build.stats.errors, 75 | }; 76 | 77 | logger.error(JSON.stringify(json, null, 2)); 78 | } else { 79 | logger.error(Chalk.red('Bundling failed')); 80 | 81 | if (build.stats.errors.length) { 82 | logger.error(build.stats.errors[0]); 83 | } 84 | } 85 | } 86 | 87 | function formatOutput(build, url) { 88 | let output; 89 | 90 | if (args.watch) { 91 | output = { 92 | generation: build.generation, 93 | container: build.webtask.container, 94 | }; 95 | 96 | if (args.showToken) { 97 | output.token = build.webtask.token; 98 | } 99 | 100 | logger.info(output, 'Webtask %s: %s', options.action, url); 101 | } else if (args.output === 'json') { 102 | output = { 103 | url: url, 104 | name: build.webtask.claims.jtn, 105 | container: build.webtask.container, 106 | }; 107 | 108 | if (args.showToken) { 109 | output.token = build.webtask.token; 110 | } 111 | 112 | logger.info(JSON.stringify(output, null, 2)); 113 | } else if (options.onOutput) { 114 | options.onOutput(logger.info, build, url); 115 | } else if (args.showToken) { 116 | logger.info(Chalk.green('Webtask token %s') + '\n\n%s\n\nYou can access your webtask at the following url:\n\n%s', options.action, Chalk.gray(build.webtask.token), Chalk.bold(url)); 117 | } else { 118 | logger.info(Chalk.green('Webtask %s') + '\n\nYou can access your webtask at the following url:\n\n%s', options.action, Chalk.bold(url)); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/cron.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Cli = require('structured-cli'); 4 | 5 | module.exports = { 6 | parseSchedule, 7 | parseTimezone, 8 | }; 9 | 10 | const intervals = { 11 | minutes: { 12 | abbrev: ['m', 'min', 'mins', 'minute', 'minutes'], 13 | values: [1, 2, 3, 4, 5, 6, 10, 15, 20, 30, 60], 14 | encode(frequencyValue) { 15 | const now = new Date(); 16 | const cron = ['*', '*', '*', '*', '*']; 17 | const curr = now.getMinutes(); 18 | const mod = curr % frequencyValue; 19 | 20 | cron[0] = 21 | frequencyValue === 60 22 | ? curr 23 | : mod > 0 24 | ? curr % frequencyValue + '-' + 59 + '/' + frequencyValue 25 | : '*/' + frequencyValue; 26 | 27 | return cron.join(' '); 28 | }, 29 | }, 30 | hours: { 31 | abbrev: ['h', 'hour', 'hours'], 32 | values: [1, 2, 3, 4, 6, 8, 12, 24], 33 | encode(frequencyValue) { 34 | const now = new Date(); 35 | const cron = [now.getMinutes(), '*', '*', '*', '*']; 36 | const curr = now.getHours(); 37 | const mod = curr % frequencyValue; 38 | 39 | cron[1] = 40 | frequencyValue === 24 41 | ? curr 42 | : mod > 0 43 | ? curr % frequencyValue + '-' + 23 + '/' + frequencyValue 44 | : '*/' + frequencyValue; 45 | 46 | return cron.join(' '); 47 | }, 48 | }, 49 | days: { 50 | abbrev: ['d', 'day', 'days'], 51 | values: [1], 52 | encode() { 53 | return intervals.hours.encode(24); 54 | }, 55 | }, 56 | }; 57 | 58 | function parseSchedule(schedule) { 59 | if (schedule.split(' ').length !== 5) { 60 | const minutesRx = new RegExp( 61 | '^\\s*([0-9]{1,2})\\s*(' + 62 | intervals.minutes.abbrev.join('|') + 63 | ')\\s*$', 64 | 'i' 65 | ); 66 | const hoursRx = new RegExp( 67 | '^\\s*([0-9]{1,2})\\s*(' + 68 | intervals.hours.abbrev.join('|') + 69 | ')\\s*$', 70 | 'i' 71 | ); 72 | const daysRx = new RegExp( 73 | '^\\s*([0-9]{1,2})\\s*(' + 74 | intervals.days.abbrev.join('|') + 75 | ')\\s*$', 76 | 'i' 77 | ); 78 | let frequencyValue; 79 | let type; 80 | let matches; 81 | 82 | if ((matches = schedule.match(minutesRx))) { 83 | type = intervals.minutes; 84 | frequencyValue = parseInt(matches[1], 10); 85 | } else if ((matches = schedule.match(hoursRx))) { 86 | type = intervals.hours; 87 | frequencyValue = parseInt(matches[1], 10); 88 | } else if ((matches = schedule.match(daysRx))) { 89 | type = intervals.days; 90 | frequencyValue = parseInt(matches[1], 10); 91 | } else { 92 | throw new Cli.error.invalid( 93 | 'The schedule `' + schedule + '` is not valid.' 94 | ); 95 | } 96 | 97 | if (type.values.indexOf(frequencyValue) === -1) { 98 | throw new Cli.error.invalid( 99 | 'For intervals in ' + 100 | type.abbrev[type.abbrev.length - 1] + 101 | ', the following intervals are supported: ' + 102 | type.values.join(', ') 103 | ); 104 | } 105 | 106 | schedule = type.encode(frequencyValue); 107 | } 108 | 109 | return schedule; 110 | } 111 | 112 | function parseTimezone(tz) { 113 | const Moment = require('moment-timezone'); 114 | 115 | if (!Moment.tz.zone(tz)) { 116 | throw new Cli.error.invalid( 117 | `The timezone "${tz}" is not recognized. Please specify a valid IANA timezone name (see: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).` 118 | ); 119 | } 120 | 121 | return tz; 122 | } 123 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | exports.cancelled = cancelled; 2 | exports.invalid = invalid; 3 | exports.notFound = notFound; 4 | exports.notAuthorized = notAuthorized; 5 | 6 | 7 | // Exported interface 8 | 9 | function cancelled(message, data) { 10 | return createError(message, 'E_CANCELLED', data, cancelled); 11 | } 12 | 13 | function invalid(message, data) { 14 | return createError(message, 'E_INVALID', data, invalid); 15 | } 16 | 17 | function notFound(message, data) { 18 | return createError(message, 'E_NOTFOUND', data, notFound); 19 | } 20 | 21 | function notAuthorized(message, data) { 22 | return createError(message, 'E_NOTAUTHORIZED', data, notAuthorized); 23 | } 24 | 25 | 26 | // Private helper functions 27 | 28 | function createError(message, code, data, ctor) { 29 | var error = new Error(message ? message : undefined); 30 | 31 | Error.captureStackTrace(error, ctor); 32 | 33 | error.code = code; 34 | error.data = data; 35 | 36 | return error; 37 | } -------------------------------------------------------------------------------- /lib/keyValList2Object.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (args, field) { 4 | args[field] = _.reduce(args[field], function (acc, entry) { 5 | var parts = entry.split('='); 6 | 7 | return _.set(acc, parts.shift(), parts.join('=')); 8 | }, {}); 9 | }; -------------------------------------------------------------------------------- /lib/logs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Os = require('os'); 4 | const PrettyStream = require('bunyan-prettystream'); 5 | const Util = require('util'); 6 | const _ = require('lodash'); 7 | 8 | 9 | exports.createLogStream = createLogStream; 10 | 11 | 12 | function createLogStream(logStream, options) { 13 | if (!options) options = {}; 14 | 15 | var logger = options.raw 16 | ? console 17 | : createBunyanLogger(); 18 | 19 | logStream.once('open', function () { 20 | logger.info({ container: options.container }, 21 | 'connected to streaming logs'); 22 | }); 23 | 24 | logStream.once('close', function () { 25 | if (typeof logger.emit === 'function') { 26 | logger.emit('close'); 27 | } 28 | }); 29 | 30 | logStream.once('error', function (err) { 31 | if (typeof logger.emit === 'function') { 32 | logger.emit('error', err); 33 | } 34 | }); 35 | 36 | logStream.on('data', function (data) { 37 | options.verbose 38 | ? logger.info(data) 39 | : logger.info(data.msg); 40 | }); 41 | 42 | return logger; 43 | } 44 | 45 | 46 | function createBunyanLogger() { 47 | var prettyStdOut = new PrettyStream({ mode: 'short' }); 48 | 49 | prettyStdOut.trace = mkLogEmitter(10); 50 | prettyStdOut.debug = mkLogEmitter(20); 51 | prettyStdOut.info = mkLogEmitter(30); 52 | prettyStdOut.warn = mkLogEmitter(40); 53 | prettyStdOut.error = mkLogEmitter(50); 54 | prettyStdOut.fatal = mkLogEmitter(60); 55 | 56 | prettyStdOut.pipe(process.stdout); 57 | 58 | return prettyStdOut; 59 | } 60 | 61 | function mkLogEmitter(minLevel) { 62 | return function() { 63 | if (arguments.length === 0) { // `log.()` 64 | return (this._level <= minLevel); 65 | } 66 | 67 | const msgArgs = new Array(arguments.length); 68 | for (let i = 0; i < msgArgs.length; ++i) { 69 | msgArgs[i] = arguments[i]; 70 | } 71 | 72 | const rec = mkRecord(minLevel, msgArgs); 73 | 74 | this.write(rec); 75 | }; 76 | } 77 | 78 | function mkRecord(level, args) { 79 | var fields, msgArgs; 80 | if (args[0] instanceof Error) { 81 | // `log.(err, ...)` 82 | fields = { 83 | // Use this Logger's err serializer, if defined. 84 | err: serializeError(args[0]), 85 | }; 86 | if (args.length === 1) { 87 | msgArgs = [fields.err.message]; 88 | } else { 89 | msgArgs = args.slice(1); 90 | } 91 | } else if (typeof (args[0]) !== 'object' || Array.isArray(args[0])) { 92 | // `log.(msg, ...)` 93 | fields = null; 94 | msgArgs = args.slice(); 95 | } else if (Buffer.isBuffer(args[0])) { // `log.(buf, ...)` 96 | // Almost certainly an error, show `inspect(buf)`. See bunyan 97 | // issue #35. 98 | fields = null; 99 | msgArgs = args.slice(); 100 | msgArgs[0] = Util.inspect(msgArgs[0]); 101 | } else { // `log.(fields, msg, ...)` 102 | fields = args[0]; 103 | if (fields && args.length === 1 && fields.err && 104 | fields.err instanceof Error) 105 | { 106 | msgArgs = [fields.err.message]; 107 | } else { 108 | msgArgs = args.slice(1); 109 | } 110 | } 111 | 112 | // Build up the record object. 113 | var rec = { 114 | level, 115 | hostname: Os.hostname(), 116 | name: 'wt', 117 | pid: process.pid, 118 | }; 119 | var recFields = (fields ? _.clone(fields) : null); 120 | if (recFields) { 121 | Object.keys(recFields).forEach(function (k) { 122 | rec[k] = recFields[k]; 123 | }); 124 | } 125 | rec.msg = Util.format.apply(Util, msgArgs); 126 | if (!rec.time) { 127 | rec.time = (new Date()); 128 | } 129 | 130 | return rec; 131 | } 132 | 133 | function serializeError(error) { 134 | if (!error || !error.stack) 135 | return error; 136 | return Object.create({ 137 | message: error.message, 138 | name: error.name, 139 | stack: getFullErrorStack(error), 140 | code: error.code, 141 | signal: error.signal 142 | }, null); 143 | } 144 | 145 | function getFullErrorStack(ex) { 146 | var ret = ex.stack || ex.toString(); 147 | if (ex.cause && typeof (ex.cause) === 'function') { 148 | var cex = ex.cause(); 149 | if (cex) { 150 | ret += '\nCaused by: ' + getFullErrorStack(cex); 151 | } 152 | } 153 | return (ret); 154 | } 155 | -------------------------------------------------------------------------------- /lib/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Bluebird = require('bluebird'); 4 | const Chalk = require('chalk'); 5 | const Cli = require('structured-cli'); 6 | const Sandbox = require('sandboxjs'); 7 | const Superagent = require('superagent'); 8 | const Wrap = require('linewrap'); 9 | const _ = require('lodash'); 10 | 11 | const ENSURE_AVAILABLE_TIMEOUT = 1000 * 60 * 5; 12 | const ENSURE_AVAILABLE_INTERVAL = 1000 * 5; 13 | const UNPKG_URL = 'https://unpkg.com'; 14 | 15 | const wrapHint = Wrap(process.stdout.columns, { 16 | skipScheme: 'ansi-color', 17 | wrapLineIndent: 2, 18 | }); 19 | 20 | module.exports = { 21 | awaitAvailable, 22 | ensure, 23 | parseSpec, 24 | provision, 25 | resolveSpec, 26 | }; 27 | 28 | function awaitAvailable(profile, modules, options) { 29 | const start = Date.now(); 30 | 31 | if (!options) { 32 | options = {}; 33 | } 34 | 35 | if (options.timeout === undefined) { 36 | options.timeout = ENSURE_AVAILABLE_TIMEOUT; 37 | } 38 | 39 | if (options.interval === undefined) { 40 | options.interval = ENSURE_AVAILABLE_INTERVAL; 41 | } 42 | 43 | return checkAvailability(); 44 | 45 | function checkAvailability() { 46 | if (+options.timeout > 0 && Date.now() > start + options.timeout) { 47 | return Bluebird.reject( 48 | Cli.error.timeout( 49 | 'Timed out waiting for modules to become available' 50 | ) 51 | ); 52 | } 53 | 54 | return ensure(profile, modules, options).then(validateAvailability); 55 | } 56 | 57 | function validateAvailability(results) { 58 | const failed = _.find(results, module => module.state === 'failed'); 59 | const queued = _.find(results, module => module.state === 'queued'); 60 | 61 | if (options.onPoll) { 62 | try { 63 | options.onPoll(results); 64 | } catch (__) { 65 | // Discard exception 66 | } 67 | } 68 | 69 | if (failed) { 70 | return Bluebird.reject( 71 | Cli.error.serverError( 72 | `The module ${failed.name}@${failed.version} failed to build.` 73 | ) 74 | ); 75 | } 76 | 77 | if (queued) { 78 | return Bluebird.delay(+options.interval).then(checkAvailability); 79 | } 80 | 81 | return results; 82 | } 83 | } 84 | 85 | function ensure(profile, modules, options) { 86 | if (!options) { 87 | options = {}; 88 | } 89 | 90 | return Bluebird.resolve( 91 | profile.ensureNodeModules({ modules, reset: !!options.reset }) 92 | ); 93 | } 94 | 95 | function parseSpec(spec) { 96 | const idx = spec.indexOf('@', 1); 97 | const name = idx === -1 ? spec : spec.slice(0, idx); 98 | const range = spec.slice(name.length + 1, spec.length) || '*'; 99 | 100 | return { name, range }; 101 | } 102 | 103 | function resolveSpec(profile, module) { 104 | const spec = `${ module.name }@${ module.range }`; 105 | const url = `${ UNPKG_URL }/${ spec }/package.json`; 106 | const request = Superagent.get(url); 107 | 108 | return profile.issueRequest(request) 109 | .catch(() => { 110 | return Bluebird.reject( 111 | Cli.error.notFound( 112 | `Error looking up module on npm that satisfies ${ spec }.` 113 | ) 114 | ); 115 | }) 116 | .get('body') 117 | .then(packageJson => { 118 | return { 119 | name: module.name, 120 | version: packageJson.version, 121 | }; 122 | }); 123 | } 124 | 125 | function provision(profile, modules, options) { 126 | options = _.defaultsDeep({}, options, { 127 | logger: { 128 | info: _.noop, 129 | warn: _.noop, 130 | error: _.noop, 131 | }, 132 | }); 133 | 134 | const logger = options.logger; 135 | 136 | if (modules.length) { 137 | logger.info(`Resolving ${ Chalk.bold(modules.length) } module${ modules.length === 1 ? '' : 's' }...`); 138 | } 139 | 140 | return Bluebird.map(modules, (mod) => resolveSpec(profile, mod)).then(modules => { 141 | const total = Object.keys(modules).length; 142 | const pluralization = total === 1 ? 'module' : 'modules'; 143 | const stateToColor = { 144 | queued: Chalk.blue, 145 | available: Chalk.green, 146 | failed: Chalk.red, 147 | }; 148 | const moduleState = {}; 149 | let polls = 0; 150 | 151 | if (!total) { 152 | return modules; 153 | } 154 | 155 | logger.info(`Provisioning ${ Chalk.bold(total) } ${ pluralization }...`); 156 | 157 | const available$ = awaitAvailable(profile, modules, { onPoll }); 158 | 159 | return available$.then(() => modules); 160 | 161 | function onPoll(modules) { 162 | const countByState = _.countBy(modules, mod => mod.state); 163 | 164 | if (polls === 0 && countByState.queued && !countByState.failed) { 165 | const pluralization = countByState.queued === 1 166 | ? 'module' 167 | : 'modules'; 168 | const verb = countByState.queued === 1 ? 'is' : 'are'; 169 | 170 | logger.info( 171 | wrapHint( 172 | Chalk.italic( 173 | `* Hint: ${countByState.queued} ${pluralization} ${verb} queued. Please be patient while we build missing modules. This could take a couple minutes and will only ever need to be done once per module version.` 174 | ) 175 | ) 176 | ); 177 | } 178 | 179 | _.forEach(modules, mod => { 180 | const modId = `${mod.name}@${mod.version}`; 181 | 182 | if ( 183 | mod.state !== 'queued' && mod.state !== moduleState[modId] 184 | ) { 185 | const color = stateToColor[mod.state]; 186 | const update = `${ Chalk.bold(modId) } is ${color(mod.state)}`; 187 | 188 | logger.info(update); 189 | } 190 | 191 | moduleState[modId] = mod.state; 192 | }); 193 | 194 | polls++; 195 | } 196 | }); 197 | } 198 | -------------------------------------------------------------------------------- /lib/printCronJob.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Chalk = require('chalk'); 4 | const Moment = require('moment-timezone'); 5 | const Pad = require('pad'); 6 | 7 | 8 | module.exports = printCronJob; 9 | 10 | 11 | function printCronJob(job) { 12 | const WIDTH = 12; 13 | 14 | console.log(Chalk.blue(Pad('Name:', WIDTH)), Chalk.green(job.name)); 15 | console.log(Chalk.blue(Pad('State:', WIDTH)), job.state); 16 | console.log(Chalk.blue(Pad('Container:', WIDTH)), job.container); 17 | console.log(Chalk.blue(Pad('Schedule:', WIDTH)), job.schedule); 18 | console.log(Chalk.blue(Pad('Timezone:', WIDTH)), job.tz); 19 | 20 | if (job.results && job.results.length) { 21 | console.log(Chalk.blue(Pad('Last result:', WIDTH)), job.results[0].type); 22 | console.log(Chalk.blue(Pad('Last run:', WIDTH)), Moment.tz(job.results[0].completed_at, job.tz).format('L LTS z')); 23 | } 24 | 25 | console.log(Chalk.blue(Pad('Next run:', WIDTH)), Moment.tz(job.next_available_at, job.tz).format('L LTS z')); 26 | 27 | if (job.expires_at) { 28 | console.log(Chalk.blue(Pad('Expires:', WIDTH)), Moment.tz(job.expires_at, job.tz).format('L LTS z')); 29 | } 30 | 31 | if (job.meta) { 32 | for (let m in job.meta) { 33 | console.log(Chalk.blue(Pad('Meta.' + m + ':', WIDTH)), job.meta[m]); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/printProfile.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Decode = require('jwt-decode'); 3 | var Pad = require('pad'); 4 | 5 | module.exports = printProfile; 6 | 7 | 8 | function printProfile(profile, options) { 9 | var WIDTH = 12; 10 | 11 | if (!options) options = {}; 12 | 13 | console.log(Chalk.blue(Pad('Profile:', WIDTH)), Chalk.green(profile.name)); 14 | console.log(Chalk.blue(Pad('URL:', WIDTH)), profile.url); 15 | console.log(Chalk.blue(Pad('Container:', WIDTH)), profile.container); 16 | 17 | if (profile.openid) { 18 | console.log(Chalk.blue(Pad('Version:', WIDTH)), Chalk.green('v2')); 19 | console.log(Chalk.blue(Pad('Scopes:', WIDTH)), Chalk.green(profile.openid.scopes.join(', '))); 20 | var still_valid = (new Date() - new Date(profile.openid.valid_until)) < 0 21 | if (still_valid) 22 | console.log(Chalk.blue(Pad('Expires:', WIDTH)), Chalk.green(profile.openid.valid_until), Chalk.green('(valid)')); 23 | else 24 | console.log(Chalk.blue(Pad('Expires:', WIDTH)), Chalk.red(profile.openid.valid_until), Chalk.red('(expired)')); 25 | } 26 | else { 27 | console.log(Chalk.blue(Pad('Version:', WIDTH)), Chalk.green('v1')); 28 | } 29 | 30 | if (options.token) { 31 | console.log(Chalk.blue(Pad('Token:', WIDTH)), profile.token); 32 | } 33 | 34 | if (options.details) { 35 | try { 36 | var claims = Decode(profile.token); 37 | var keys = Object.keys(claims).sort(); 38 | keys.forEach(function (key) { 39 | var name = 'Token.' + key + ':'; 40 | console.log(Chalk.blue(Pad(name, WIDTH)), claims[key]); 41 | }); 42 | } catch (__) { 43 | console.log(Chalk.red('Token is not a valid JWT')); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/printTokenDetails.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Decode = require('jwt-decode'); 3 | var Pad = require('pad'); 4 | 5 | 6 | module.exports = printTokenDetails; 7 | 8 | 9 | function printTokenDetails(claims, options) { 10 | var WIDTH = 12; 11 | 12 | var keys = Object.keys(claims).sort(); 13 | keys.forEach(function (key) { 14 | if (key == 'meta' || !claims[key]) return; 15 | var name = 'Token.' + key + ':'; 16 | console.log(Chalk.blue(Pad(name, WIDTH)), claims[key]); 17 | }); 18 | 19 | if (claims.meta) { 20 | for (var m in claims.meta) { 21 | console.log(Chalk.blue(Pad('Meta.' + m + ':', WIDTH)), claims.meta[m]); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/printWebtask.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Decode = require('jwt-decode'); 3 | var Pad = require('pad'); 4 | 5 | 6 | module.exports = printWebtask; 7 | 8 | 9 | function printWebtask(webtask, options) { 10 | var WIDTH = 12; 11 | 12 | if (!options) options = {}; 13 | 14 | var json = webtask.toJSON(); 15 | 16 | console.log(Chalk.blue(Pad('Name:', WIDTH)), Chalk.green(json.name)); 17 | console.log(Chalk.blue(Pad('URL:', WIDTH)), webtask.url); 18 | 19 | 20 | if (options.token) { 21 | console.log(Chalk.blue(Pad('Token:', WIDTH)), webtask.token); 22 | } 23 | 24 | if (webtask.meta && options.verbose) { 25 | for (var m in webtask.meta) { 26 | console.log(Chalk.blue(Pad('Meta.' + m + ':', WIDTH)), webtask.meta[m]); 27 | } 28 | } 29 | 30 | if (options.details) { 31 | try { 32 | var claims = Decode(webtask.token); 33 | var keys = Object.keys(claims).sort(); 34 | keys.forEach(function (key) { 35 | var name = 'Token.' + key + ':'; 36 | console.log(Chalk.blue(Pad(name, WIDTH)), claims[key]); 37 | }); 38 | } catch (__) { 39 | console.log(Chalk.red('Token is not a valid JWT')); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/printWebtaskDetails.js: -------------------------------------------------------------------------------- 1 | var Chalk = require('chalk'); 2 | var Pad = require('pad'); 3 | 4 | 5 | module.exports = printWebtaskDetails; 6 | 7 | 8 | function printWebtaskDetails(webtask) { 9 | var WIDTH = 12; 10 | 11 | var json = webtask.toJSON(); 12 | 13 | console.log(Chalk.blue(Pad('Name:', WIDTH)), Chalk.green(json.name)); 14 | console.log(Chalk.blue(Pad('URL:', WIDTH)), json.url); 15 | console.log(Chalk.blue(Pad('Container:', WIDTH)), json.container); 16 | 17 | if (webtask.meta) { 18 | for (var m in webtask.meta) { 19 | console.log(Chalk.blue(Pad('Meta.' + m + ':', WIDTH)), webtask.meta[m]); 20 | } 21 | } 22 | 23 | Object.keys(webtask.secrets || []).sort().forEach(function (s) { 24 | console.log(Chalk.blue(Pad('Secret.' + s + ':', WIDTH)), webtask.secrets[s]); 25 | }); 26 | 27 | if (webtask.code) { 28 | console.log(Chalk.blue('Code:')); 29 | console.log(webtask.code.trim()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/userAuthenticator.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Errors = require('./errors'); 3 | var Sandbox = require('sandboxjs'); 4 | var Superagent = require('superagent'); 5 | var Http = require('http'); 6 | var Url = require('url'); 7 | var Open = require('opn'); 8 | var Chalk = require('chalk'); 9 | var Decode = require('jwt-decode'); 10 | var Assert = require('assert'); 11 | var Crypto = require('crypto'); 12 | 13 | require('superagent-proxy')(Superagent) 14 | 15 | module.exports = UserAuthenticator; 16 | 17 | function UserAuthenticator (config) { 18 | if (!config) config = {}; 19 | 20 | this.sandboxUrl = config.sandboxUrl || 'https://sandbox.auth0-extend.com'; 21 | this.authorizationServer = config.authorizationServer; 22 | this.audience = config.audience; 23 | this.clientId = config.clientId; 24 | this.refreshToken = config.refreshToken; 25 | } 26 | 27 | // Discover whether WT deployment supports auth v2 and if so create 28 | // UserAuthenticator instance for it 29 | UserAuthenticator.create = function (sandboxUrl, auth0) { 30 | // we are defaulting to AUTHv2 here, we can revert to AUTHv1 using 31 | // AUTH_MODE=v1 env var the use of --auth0 switch forces AUTHv2 as well. 32 | 33 | if (process.env.AUTH_MODE === 'v1' && !auth0) return Promise.resolve(null); 34 | 35 | var descriptionUrl = Url.parse(sandboxUrl); 36 | descriptionUrl.pathname = '/api/description'; 37 | let request = Superagent 38 | .get(Url.format(descriptionUrl)) 39 | 40 | const proxy = process.env.http_proxy || process.env.HTTP_PROXY; 41 | if (proxy) { 42 | request = request.proxy(proxy); 43 | } 44 | 45 | return request 46 | .ok(res => res.status < 500) 47 | .then(res => { 48 | if (res.status === 200 && res.body && res.body.authorization_server) { 49 | return new UserAuthenticator({ 50 | sandboxUrl, 51 | authorizationServer: res.body.authorization_server, 52 | audience: res.body.audience || sandboxUrl, 53 | clientId: res.body.client_id, 54 | }); 55 | } 56 | else { 57 | return Promise.resolve(null); 58 | } 59 | }); 60 | }; 61 | 62 | UserAuthenticator.prototype.login = function (options) { 63 | options = options || {}; 64 | 65 | if (this.refreshToken) { 66 | return this._refreshFlow(options); 67 | } 68 | else { 69 | return this._authorizationFlow(options); 70 | } 71 | }; 72 | 73 | // Refresh token flow 74 | UserAuthenticator.prototype._refreshFlow = function (options) { 75 | var refreshUrl = Url.parse(this.authorizationServer); 76 | refreshUrl.pathname = '/oauth/token'; 77 | var self = this; 78 | 79 | let request = Superagent 80 | .post(Url.format(refreshUrl)) 81 | 82 | const proxy = process.env.http_proxy || process.env.HTTP_PROXY; 83 | if (proxy) { 84 | request = request.proxy(proxy); 85 | } 86 | 87 | return request 88 | .send({ 89 | grant_type: 'refresh_token', 90 | client_id: this.clientId, 91 | refresh_token: this.refreshToken, 92 | }) 93 | .then(res => { 94 | console.log('Your access token was successfuly refreshed.'); 95 | return self._processAccessTokenResponse(options, res.body, options.requestedScopes); 96 | }) 97 | .catch(e => { 98 | // In case of any error during refresh token flow, fall back on 99 | // regular authorization flow 100 | console.log(`Failure trying to refresh the access token: ${e.message}`); 101 | return self._authorizationFlow(options); 102 | }); 103 | }; 104 | 105 | // Browser based authorization flow 106 | UserAuthenticator.prototype._authorizationFlow = function (options) { 107 | // Initialize PKCE authorization flow through a browser 108 | 109 | var self = this; 110 | var codeVerifier = base64URLEncode(Crypto.randomBytes(16)); 111 | var codeChallange = base64URLEncode(Crypto.createHash('sha256').update(codeVerifier).digest()); 112 | var port = 8722 + Math.floor(6 * Math.random()); 113 | var redirectUri = `http://127.0.0.1:${port}`; 114 | var requestedScopes = [ 'openid', 'offline_access' ]; 115 | var audience; 116 | if (options.auth0) { 117 | audience = Url.parse(self.audience || self.sandboxUrl); 118 | audience.pathname += '/containers/' + options.container; 119 | audience = Url.format(audience); 120 | requestedScopes.push('wt:owner'); 121 | } 122 | else { 123 | audience = self.audience || self.sandboxUrl; 124 | if (options.container) { 125 | requestedScopes.push(`wt:owner:${options.container}`); 126 | } 127 | if (options.admin) { 128 | requestedScopes.push(`wt:admin`); 129 | } 130 | } 131 | requestedScopes = requestedScopes.join(' '); 132 | 133 | var onceServer$ = createOnceServer(); 134 | var loginUrl = createLoginUrl(); 135 | 136 | console.log('Attempting to open the following login url in your browser: '); 137 | console.log(); 138 | console.log(Chalk.underline(loginUrl)); 139 | console.log(); 140 | console.log('If the browser does not automatically open, please copy this address and paste it into your browser.'); 141 | 142 | Open(loginUrl, { wait: false }); 143 | 144 | return onceServer$; 145 | 146 | // Create PKCE login URL 147 | function createLoginUrl() { 148 | var loginUrl = Url.parse(self.authorizationServer, true); 149 | loginUrl.pathname = '/authorize'; 150 | loginUrl.query = { 151 | redirect_uri: redirectUri, 152 | audience: audience, 153 | response_type: 'code', 154 | client_id: self.clientId, 155 | scope: requestedScopes, 156 | code_challenge: codeChallange, 157 | code_challenge_method: 'S256', 158 | }; 159 | 160 | return Url.format(loginUrl); 161 | } 162 | 163 | // Returns a promise that resolves when a transient, localhost HTTP server 164 | // receives the first request. This request is a redirect from the authorization server. 165 | function createOnceServer() { 166 | return new Promise((resolve, reject) => { 167 | var server = Http.createServer((req, res) => { 168 | 169 | return processRedirectCallback(req, done); 170 | 171 | var _done; 172 | function done(error, data) { 173 | if (_done) return; 174 | _done = true; 175 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 176 | if (error) { 177 | res.end(error.message); 178 | } 179 | else { 180 | res.end('Authentication successful.'); 181 | } 182 | server.close(); 183 | return error ? reject(error) : resolve(data); 184 | } 185 | }).listen(port, (e) => { 186 | if (e) reject(e); 187 | }); 188 | }); 189 | } 190 | 191 | // Process redirect from authorization server to get authorization code 192 | function processRedirectCallback(req, done) { 193 | var url = Url.parse(req.url, true); 194 | if (req.method !== 'GET' || url.pathname !== '/') { 195 | return done(new Error(`Authentication failed. Invalid redirect from authorization server: ${req.method} ${req.url}`)); 196 | } 197 | 198 | if (url.query.error) { 199 | return done(new Error(`Authentication failed: ${url.query.error}.`)); 200 | } 201 | if (!url.query.code) { 202 | return done(new Error(`Authentication failed. Authorization server response does not specify authorization code: ${req.url}.`)); 203 | } 204 | 205 | return exchangeAuthorizationCode(url.query.code, done); 206 | } 207 | 208 | // Exchange authorization code for access token using PKCE 209 | function exchangeAuthorizationCode(code, done) { 210 | var tokenUrl = Url.parse(self.authorizationServer); 211 | tokenUrl.pathname = '/oauth/token'; 212 | 213 | let request = Superagent 214 | .post(Url.format(tokenUrl)) 215 | 216 | const proxy = process.env.http_proxy || process.env.HTTP_PROXY; 217 | if (proxy) { 218 | request = request.proxy(proxy); 219 | } 220 | 221 | return request 222 | .send({ 223 | grant_type: 'authorization_code', 224 | client_id: self.clientId, 225 | code, 226 | code_verifier: codeVerifier, 227 | redirect_uri: redirectUri 228 | }) 229 | .end((e,r) => { 230 | if (e) return done(new Error(`Authentication failed. Unable to obtian access token: ${e.message}.`)); 231 | return self._processAccessTokenResponse(options, r.body, requestedScopes, done); 232 | }); 233 | } 234 | 235 | }; 236 | 237 | // Prepare wt-cli profile from rewfresh token or authorization code exchange response 238 | UserAuthenticator.prototype._processAccessTokenResponse = function(options, body, requestedScopes, done) { 239 | var scopes = (body.scope || requestedScopes).split(' '); 240 | body.scopes = scopes; 241 | var isAdmin = scopes.indexOf('wt:admin') > -1; 242 | var container; 243 | if (options.container) { 244 | if (isAdmin || scopes.indexOf(`wt:owner:${options.container}`) > -1 || scopes.indexOf('wt:owner') > -1) { 245 | container = options.container; 246 | } 247 | else { 248 | return done(new Error(`Authentication failed: user does not have permissions to container '${options.container}'.`)); 249 | } 250 | } 251 | else { 252 | if (isAdmin) { 253 | container = 'master'; 254 | } 255 | else { 256 | scopes.forEach(s => { 257 | var match = s.match(/^wt\:owner\:(.+)$/); 258 | if (match && !container) { 259 | container = match[1]; 260 | } 261 | }); 262 | if (!container) { 263 | return done(new Error(`Authentication failed: user has no permissions in the system.`)); 264 | } 265 | } 266 | } 267 | 268 | var profile = Sandbox.init({ 269 | url: this.sandboxUrl, 270 | token: body.access_token, 271 | container: container, 272 | }); 273 | profile.name = options.profileName; 274 | profile.openid = body; 275 | if (profile.openid.expires_in) { 276 | profile.openid.valid_until = new Date(Date.now() + +profile.openid.expires_in * 1000).toString(); 277 | } 278 | profile.openid.authorization_server = this.authorizationServer; 279 | profile.openid.audience = this.audience; 280 | profile.openid.client_id = this.clientId; 281 | profile.openid.refresh_token = profile.openid.refresh_token || this.refreshToken; 282 | profile.openid.auth0 = options.auth0; 283 | 284 | return done ? done(null, profile) : profile; 285 | }; 286 | 287 | function base64URLEncode(str) { 288 | return str.toString('base64') 289 | .replace(/\+/g, '-') 290 | .replace(/\//g, '_') 291 | .replace(/=/g, ''); 292 | } 293 | -------------------------------------------------------------------------------- /lib/userVerifier.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Decode = require('jwt-decode'); 3 | var Errors = require('./errors'); 4 | var Sandbox = require('sandboxjs'); 5 | var Superagent = require('superagent'); 6 | 7 | require('superagent-proxy')(Superagent) 8 | 9 | 10 | var VERIFIER_URL = 'https://auth0-extend.sandbox.auth0-extend.com/webtask-cli-verifier'; 11 | 12 | 13 | module.exports = UserVerifier; 14 | 15 | 16 | function UserVerifier (options) { 17 | } 18 | 19 | UserVerifier.prototype._runVerifierWebtask = function (query) { 20 | var request = Superagent 21 | .get(VERIFIER_URL) 22 | .query(query); 23 | 24 | const proxy = process.env.http_proxy || process.env.HTTP_PROXY; 25 | if (proxy) { 26 | request = request.proxy(proxy); 27 | } 28 | 29 | return Sandbox.issueRequest(request) 30 | .get('body'); 31 | }; 32 | 33 | UserVerifier.prototype.requestVerificationCode = function (phoneOrEmail, cb) { 34 | var self = this; 35 | var type; 36 | var value; 37 | 38 | if (UserVerifier.isPhone(phoneOrEmail)) { 39 | if (phoneOrEmail.indexOf('+') !== 0) 40 | phoneOrEmail = '+1' + phoneOrEmail; // default to US 41 | type = 'phone'; 42 | value = phoneOrEmail; 43 | } else if (UserVerifier.isEmail(phoneOrEmail)) { 44 | type = 'email'; 45 | value = phoneOrEmail; 46 | } else { 47 | var error$ = Bluebird.reject(new Errors.invalid('You must specify a valid e-mail address ' 48 | + 'or a phone number. The phone number must start with + followed ' 49 | + 'by country code, area code, and local number.')); 50 | 51 | return cb ? error$.nodeify(cb) : error$; 52 | } 53 | 54 | var payload = {}; 55 | 56 | payload[type] = value; 57 | 58 | var self = this; 59 | var promise$ = this._runVerifierWebtask(payload) 60 | .then(function (body) { 61 | // Return a function that can be called to verify the identification 62 | // code. 63 | return function verify (code, cb) { 64 | var payload = { verification_code: code }; 65 | payload[type] = value; 66 | payload.node = '8'; 67 | var verified$ = self._runVerifierWebtask(payload) 68 | .get('id_token') 69 | .then(Decode) 70 | .get('webtask'); // To get the webtask info 71 | 72 | return cb ? verified$.nodeify(cb) : verified$; 73 | }; 74 | }); 75 | 76 | return cb ? promise$.nodeify(cb) : promise$; 77 | }; 78 | 79 | UserVerifier.isPhone = function (value) { 80 | return value && !!value.match(/^\+?[0-9]{1,15}$/); 81 | }; 82 | 83 | UserVerifier.isEmail = function (value) { 84 | return !!value.match(/[^@]+@[^@]+/i); 85 | }; 86 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MIDDLEWARE_SPEC_RX = /^(@[^/]+\/[^/@]+|[^@/]+)(?:@([^/]+))?(?:\/([^/(]+))?$/; 4 | 5 | module.exports = { 6 | parseMiddleware, 7 | }; 8 | 9 | function parseMiddleware(spec) { 10 | const matches = spec.match(MIDDLEWARE_SPEC_RX); 11 | 12 | if (!matches) { 13 | throw new Error(`Failed to parse middleware spec: ${spec}`); 14 | } 15 | 16 | const moduleName = matches[1]; 17 | const moduleVersion = matches[2] || '*'; 18 | const exportName = matches[3]; 19 | 20 | return { moduleName, moduleVersion, exportName }; 21 | } 22 | -------------------------------------------------------------------------------- /lib/validateCreateArgs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Cli = require('structured-cli'); 5 | const Dotenv = require('dotenv'); 6 | const Fs = require('fs'); 7 | const Path = require('path'); 8 | const Util = require('./util'); 9 | const keyValList2Object = require('./keyValList2Object'); 10 | 11 | const MIDDLWARE_COMPILER = '@webtask/middleware-compiler'; 12 | const MIDDLWARE_COMPILER_VERSION = '^1.2.1'; 13 | const VALID_DNS_REGEX = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; 14 | const VALID_IP_REGEX = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/; 15 | const VALID_JTN_REGEX = /^[-a-z0-9._~!$+=:@]+$/i; 16 | 17 | module.exports = validateCreateArgs; 18 | 19 | 20 | function validateCreateArgs(args) { 21 | if (!args.syntheticDependencies) { 22 | args.syntheticDependencies = {}; 23 | } 24 | 25 | if (args.profile.openid) { 26 | 27 | // Enforce legacy options are not used 28 | 29 | if (args.params && args.params.length > 0) { 30 | throw Cli.error.invalid('--param is not supported by the server'); 31 | } 32 | 33 | if (args.merge !== undefined && !args.merge) { 34 | throw Cli.error.invalid('--no-merge is not supported by the server'); 35 | } 36 | 37 | if (args.parse) { 38 | throw Cli.error.invalid('--parse and --no-parse is not supported by the server'); 39 | } 40 | 41 | if (args.parseBody) { 42 | throw Cli.error.invalid('--parse-body is not supported by the server'); 43 | } 44 | 45 | } 46 | 47 | if (args.host && !args.host.match(VALID_IP_REGEX) && !args.host.match(VALID_DNS_REGEX)) { 48 | throw Cli.error.invalid('--host must specify a valid IP address or DNS domain name'); 49 | } 50 | 51 | if (args.capture && args.watch) { 52 | throw Cli.error.invalid('--watch is incompatible with --capture'); 53 | } 54 | 55 | if (!args.bundle && args.noTranspile) { 56 | throw Cli.error.invalid('--no-transpile requires --bundle'); 57 | } 58 | 59 | if (args.capture && args.bundle) { 60 | throw Cli.error.invalid('--bundle is incompatible with --capture'); 61 | } 62 | 63 | if (!args.file_or_url && args.bundle) { 64 | throw Cli.error.invalid('--bundle can not be used when reading from `stdin`'); 65 | } 66 | 67 | if (!args.file_or_url && args.watch) { 68 | throw Cli.error.invalid('--watch can not be used when reading from `stdin`'); 69 | } 70 | 71 | const fileOrUrl = args.file_or_url; 72 | 73 | if (fileOrUrl && fileOrUrl.match(/^https?:\/\//i)) { 74 | if (args.watch) { 75 | throw Cli.error.invalid('The --watch option can only be used ' 76 | + 'when a file name is specified'); 77 | } 78 | 79 | if (args.bundle && !args.capture) { 80 | throw Cli.error.invalid('The --bundle option can only be used ' 81 | + 'when a file name is specified'); 82 | } 83 | 84 | args.source = 'url'; 85 | args.spec = fileOrUrl; 86 | args.packageJsonPath = null; 87 | } else if (fileOrUrl) { 88 | if (args.capture) { 89 | throw Cli.error.invalid('The --capture option can only be used ' 90 | + 'when a url is specified'); 91 | } 92 | 93 | const resolvedPath = Path.resolve(process.cwd(), Path.dirname(fileOrUrl)); 94 | const packageJsonPath = Path.join(resolvedPath, 'package.json'); 95 | 96 | try { 97 | args.source = 'file'; 98 | args.spec = require.resolve(Path.resolve(process.cwd(), fileOrUrl)); 99 | args.packageJsonPath = !args.ignorePackageJson && Fs.existsSync(packageJsonPath) && packageJsonPath; 100 | } catch (e) { 101 | throw new Cli.error.invalid(`Error resolving the path to the webtask code '${ fileOrUrl }'`); 102 | } 103 | } else { 104 | const packageJsonPath = Path.join(process.cwd(), 'package.json'); 105 | 106 | args.source = 'stdin'; 107 | args.spec = process.cwd(); 108 | args.packageJsonPath = !args.ignorePackageJson && Fs.existsSync(packageJsonPath) && packageJsonPath; 109 | } 110 | 111 | if (!Array.isArray(args.dependencies)) { 112 | args.dependencies = []; 113 | } 114 | 115 | if (!Array.isArray(args.middleware)) { 116 | args.middleware = []; 117 | } 118 | 119 | if (!args.name && args.packageJsonPath) { 120 | // Attempt to set the webtask name based on the name from the package.json 121 | try { 122 | const packageJson = require(args.packageJsonPath); 123 | 124 | args.name = packageJson.name; 125 | } catch (__) { 126 | // Do nothing 127 | } 128 | } 129 | 130 | // If the name could not be derived from arguments or from package.json, 131 | // then try to derive it from either the filename or, as a last resort, 132 | // create a hash of the 'path / url'. 133 | if (!args.name) { 134 | // The md5 approach is here for redundancy, but in practice, it seems 135 | // that Path.basename() will resolve to something intelligent all the 136 | // time. 137 | args.name = Path.basename(args.spec, Path.extname(args.spec)) || require('crypto') 138 | .createHash('md5') 139 | .update(args.spec) 140 | .digest('hex'); 141 | } else if (!VALID_JTN_REGEX.test(args.name)) { 142 | throw new Cli.error.invalid(`Invalid webtask name '${args.name}'.`); 143 | } 144 | 145 | keyValList2Object(args, 'secrets'); 146 | keyValList2Object(args, 'params'); 147 | keyValList2Object(args, 'meta'); 148 | 149 | if (args.secretsFile) { 150 | try { 151 | const filename = Path.resolve(process.cwd(), args.secretsFile); 152 | const content = Fs.readFileSync(filename, 'utf8'); 153 | const secrets = Dotenv.parse(content); 154 | 155 | for (let secret in secrets) { 156 | if (!args.secrets.hasOwnProperty(secret)) { 157 | args.secrets[secret] = secrets[secret]; 158 | } 159 | } 160 | } catch (e) { 161 | throw Cli.error.invalid(`Error loading secrets file: ${e.message}`); 162 | } 163 | } 164 | 165 | if (args.metaFile) { 166 | try { 167 | const filename = Path.resolve(process.cwd(), args.metaFile); 168 | const content = Fs.readFileSync(filename, 'utf8'); 169 | const meta = Dotenv.parse(content); 170 | 171 | for (let key in meta) { 172 | if (!args.meta.hasOwnProperty(key)) { 173 | args.meta[key] = meta[key]; 174 | } 175 | } 176 | } catch (e) { 177 | throw Cli.error.invalid(`Error loading meta file: ${e.message}`); 178 | } 179 | } 180 | 181 | if (args.middleware.length) { 182 | if (args.meta && args.meta['wt-compiler'] && args.meta['wt-compiler'] !== MIDDLWARE_COMPILER) { 183 | throw Cli.error.invalid('Use of middleware is incompatible with a custom webtask compiler.'); 184 | } 185 | const filter = mw => _.startsWith(mw, 'http') ? 'urls' : 'modules'; 186 | const groups = _.groupBy(args.middleware, filter); 187 | const middleware = []; 188 | 189 | args.meta['wt-compiler'] = MIDDLWARE_COMPILER; 190 | args.syntheticDependencies[MIDDLWARE_COMPILER] = MIDDLWARE_COMPILER_VERSION; 191 | 192 | (groups.modules||[]) 193 | .map(Util.parseMiddleware) 194 | .forEach(middlewareRef => { 195 | const specWithoutVersion = [middlewareRef.moduleName, middlewareRef.exportName].filter(Boolean).join('/'); 196 | middleware.push(specWithoutVersion); 197 | args.syntheticDependencies[middlewareRef.moduleName] = middlewareRef.moduleVersion; 198 | }); 199 | args.meta['wt-middleware'] = _.union(middleware, groups.urls).join(','); 200 | } 201 | 202 | return args; 203 | } 204 | -------------------------------------------------------------------------------- /lib/webtaskCreator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Bluebird = require('bluebird'); 4 | const Bundler = require('webtask-bundle'); 5 | const Cli = require('structured-cli'); 6 | const Concat = require('concat-stream'); 7 | const Errors = require('./errors'); 8 | const Fs = Bluebird.promisifyAll(require('fs')); 9 | const Modules = require('./modules'); 10 | const Sandbox = require('sandboxjs'); 11 | const Superagent = require('superagent'); 12 | const Watcher = require('filewatcher'); 13 | const _ = require('lodash'); 14 | 15 | module.exports = createWebtaskCreator; 16 | 17 | 18 | function createWebtaskCreator(args, options) { 19 | options = _.defaultsDeep({}, options, { 20 | onError: _.noop, 21 | onGeneration: _.noop, 22 | logger: { 23 | info: _.noop, 24 | warn: _.noop, 25 | error: _.noop, 26 | }, 27 | }); 28 | 29 | const logger = options.logger; 30 | 31 | return createWebtask; 32 | 33 | 34 | function checkNodeModules(profile) { 35 | if (args.meta && args.meta['wt-node-dependencies']) { 36 | try { 37 | args.syntheticDependencies = Object.assign({}, args.syntheticDependencies, JSON.parse(args.meta['wt-node-dependencies'])); 38 | } catch (error) { 39 | throw new Cli.error.serverError(`Failed to read wt-node-dependencies: ${ error.message }`); 40 | } 41 | } 42 | 43 | if (args.dependencies.length || (args.syntheticDependencies && !args.packageJsonPath)) { 44 | const modules = args.dependencies.map(spec => Modules.parseSpec(spec)); 45 | 46 | if (args.syntheticDependencies) { 47 | for (let name in args.syntheticDependencies) { 48 | const range = args.syntheticDependencies[name]; 49 | 50 | // Explicit dependencies should override synthetic dependencies 51 | if (!_.find(modules, mod => mod.name === name && mod.range !== range)) { 52 | modules.push({ name, range }); 53 | } 54 | } 55 | } 56 | 57 | return Modules.provision(profile, modules, { logger }) 58 | .tap(dependencies => { 59 | if (!dependencies.length) return; 60 | 61 | const resolved = dependencies.reduce((resolved, dependency) => { 62 | resolved[dependency.name] = dependency.version; 63 | return resolved; 64 | }, {}); 65 | 66 | args.resolvedDependencies = resolved; 67 | args.meta['wt-node-dependencies'] = JSON.stringify(resolved); 68 | }, (error) => { 69 | throw new Cli.error.serverError(`Failed to provision node modules: ${ error.message }`); 70 | }); 71 | } else if (args.packageJsonPath && !args.ignorePackageJson) { 72 | return Fs.readFileAsync(args.packageJsonPath) 73 | .then(packageJsonBuffer => { 74 | let packageJson; 75 | try { 76 | packageJson = JSON.parse(packageJsonBuffer.toString('utf8')); 77 | } catch (e) { 78 | throw new Cli.error.badRequest('A package.json file was detected but could not be parsed as valid json'); 79 | } 80 | const dependencies = Object.assign({}, args.syntheticDependencies, packageJson.dependencies); 81 | const modules = _.map(dependencies, (range, name) => ({ name, range })); 82 | 83 | return Modules.provision(profile, modules, { logger }) 84 | .tap(dependencies => { 85 | if (!dependencies.length) return; 86 | 87 | const resolved = dependencies.reduce((resolved, dependency) => { 88 | resolved[dependency.name] = dependency.version; 89 | return resolved; 90 | }, {}); 91 | 92 | args.resolvedDependencies = resolved; 93 | args.meta['wt-node-dependencies'] = JSON.stringify(resolved); 94 | }, (error) => { 95 | throw new Cli.error.serverError(`Failed to provision node modules: ${ error.message }`); 96 | }); 97 | }); 98 | } 99 | 100 | return Promise.resolve([]); 101 | } 102 | 103 | 104 | 105 | function createWebtask(profile) { 106 | return args.source === 'url' 107 | ? createSimpleWebtask(profile) 108 | : createLocalWebtask(profile); 109 | } 110 | 111 | function createSimpleWebtask(profile, generation) { 112 | const codeOrUrl$ = args.capture 113 | ? profile.issueRequest(Superagent.get(args.spec)).get('text') 114 | : args.source === 'stdin' 115 | ? readStdin() 116 | : args.source === 'file' 117 | ? Fs.readFileAsync(args.spec, 'utf8') 118 | : Bluebird.resolve(args.spec); 119 | 120 | const webtask$ = codeOrUrl$ 121 | .tap(() => checkNodeModules(profile)) 122 | .then(function (codeOrUrl) { 123 | return putWebtask(profile, args, codeOrUrl); 124 | }) 125 | .catch(function (e) { 126 | return e && e.statusCode === 403; 127 | }, function (e) { 128 | throw Errors.notAuthorized('Error creating webtask: ' + e.message); 129 | }); 130 | 131 | return webtask$ 132 | .tap(function (webtask) { 133 | return options.onGeneration({ 134 | generation: +generation, 135 | webtask: webtask, 136 | }); 137 | }); 138 | 139 | 140 | function readStdin() { 141 | return new Bluebird(function (resolve, reject) { 142 | if (process.stdin.isTTY) { 143 | return reject(Cli.error.invalid('Code must be piped in when no code or url is specified')); 144 | } 145 | 146 | const concat = Concat({ encoding: 'string' }, resolve); 147 | 148 | concat.once('error', reject); 149 | 150 | process.stdin.pipe(concat); 151 | }); 152 | } 153 | } 154 | 155 | function putWebtask(profile, args, codeOrUrl) { 156 | if (profile.openid) { 157 | // The POST /api/tokens/issue is not supported with auth v2, 158 | // use PUT /api/webtask/:tenant/:name instead. 159 | var payload = {}; 160 | if (args.secrets && Object.keys(args.secrets).length > 0) payload.secrets = args.secrets; 161 | if (args.meta && Object.keys(args.meta).length > 0) payload.meta = args.meta; 162 | if (args.host) payload.host = args.host; 163 | payload[(codeOrUrl.indexOf('http://') === 0 || codeOrUrl.indexOf('https://') === 0) ? 'url' : 'code'] = codeOrUrl; 164 | 165 | const request = Superagent 166 | .put(`${profile.url}/api/webtask/${profile.container}/${args.name}`) 167 | .set('Authorization', `Bearer ${profile.token}`) 168 | .send(payload) 169 | 170 | return profile.issueRequest(request) 171 | .then(res => new Sandbox.Webtask(profile, res.body.token, { 172 | name: args.name, 173 | meta: res.body.meta, 174 | webtask_url: res.body.webtask_url, 175 | })); 176 | } 177 | else { 178 | return profile.create(codeOrUrl, { 179 | name: args.name, 180 | merge: args.merge, 181 | parse: (args.parseBody || args.parse) !== undefined ? +(args.parseBody || args.parse) : undefined, 182 | secrets: args.secrets, 183 | params: args.params, 184 | meta: args.meta, 185 | host: args.host 186 | }); 187 | } 188 | } 189 | 190 | function checkNodeVersion(profile) { 191 | const request = Superagent.post( 192 | `${profile.url}/api/run/${profile.container}` 193 | ) 194 | .set("Authorization", `Bearer ${profile.token}`) 195 | .send( 196 | `module.exports = cb => cb(null, { version: process.version.replace(/^v/,'') });` 197 | ); 198 | 199 | return profile.issueRequest(request).then((res) => res.body.version); 200 | } 201 | 202 | function createLocalWebtask(profile) { 203 | return args.bundle 204 | ? checkNodeModules(profile) 205 | .then(() => checkNodeVersion(profile)) 206 | .then(nodeVersion => createBundledWebtask(profile, nodeVersion)) 207 | : createSimpleFileWebtask(profile); 208 | } 209 | 210 | function createBundledWebtask(profile, nodeVersion) { 211 | let lastGeneration; 212 | 213 | return Bundler.bundle({ 214 | dependencies: args.resolvedDependencies || {}, 215 | disableTranspilation: args.noTranspile, 216 | entry: args.spec, 217 | minify: args.minify, 218 | name: args.name, 219 | nodeVersion, 220 | watch: args.watch, 221 | }) 222 | .switchMap(onGeneration) 223 | .toPromise(Bluebird); 224 | 225 | function onGeneration(build) { 226 | lastGeneration = build.generation; 227 | 228 | if (build.stats.errors.length) { 229 | options.onError(build); 230 | 231 | return Bluebird.resolve(); 232 | } 233 | 234 | return putWebtask(profile, args, build.code) 235 | .then(_.partial(onWebtask, lastGeneration), _.partial(onWebtaskError, lastGeneration)); 236 | 237 | 238 | function onWebtask(generation, webtask) { 239 | if (lastGeneration === generation) { 240 | return Bluebird.resolve(options.onGeneration({ 241 | generation: generation, 242 | webtask: webtask, 243 | })); 244 | } 245 | } 246 | 247 | function onWebtaskError(generation, err) { 248 | if (lastGeneration === generation) { 249 | // eslint-disable-next-line no-console 250 | console.error('onWebtaskError', err.stack); 251 | throw err; 252 | } 253 | } 254 | } 255 | } 256 | 257 | function createSimpleFileWebtask(profile) { 258 | return args.watch 259 | ? createWatchedFileWebtask(profile) 260 | : createSimpleWebtask(profile); 261 | } 262 | 263 | function createWatchedFileWebtask(profile) { 264 | return new Bluebird(function (resolve, reject) { 265 | 266 | const watcher = Watcher(); 267 | let generation = 0; 268 | let queue = Bluebird.resolve(); 269 | 270 | watcher.add(args.spec); 271 | 272 | if (args.packageJsonPath) { 273 | watcher.add(args.packageJsonPath); 274 | } 275 | 276 | watcher.on('change', onChange); 277 | watcher.on('error', onError); 278 | 279 | onChange(); 280 | 281 | function onChange() { 282 | queue = queue.then(function () { 283 | const webtask$ = createSimpleWebtask(profile, ++generation); 284 | 285 | return webtask$ 286 | .catch(onError); 287 | }); 288 | } 289 | 290 | function onError(err) { 291 | watcher.removeAll(); 292 | 293 | reject(err); 294 | } 295 | }); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /opslevel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | repository: 4 | owner: dx_extensibility 5 | tags: 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wt-cli", 3 | "author": { 4 | "name": "Geoff Goodman ", 5 | "url": "https://github.com/ggoodman", 6 | "twitter": "filearts" 7 | }, 8 | "version": "12.3.0", 9 | "description": "Webtask Command Line Interface", 10 | "tags": [ 11 | "nodejs", 12 | "webtask", 13 | "auth0", 14 | "nodejs", 15 | "node" 16 | ], 17 | "engines": { 18 | "node": ">=8.9.0" 19 | }, 20 | "license": "MIT", 21 | "scripts": { 22 | "test": "echo \"Error: no test specified\" && exit 1" 23 | }, 24 | "dependencies": { 25 | "auth0-hooks-templates": "1.2.2", 26 | "bluebird": "2.11.0", 27 | "bunyan-prettystream": "0.1.3", 28 | "chalk": "1.1.3", 29 | "concat-stream": "1.6.2", 30 | "dotenv": "1.2.0", 31 | "filewatcher": "3.0.1", 32 | "jwt-decode": "1.5.1", 33 | "linewrap": "0.2.1", 34 | "lodash": "4.17.21", 35 | "moment-timezone": "0.5.21", 36 | "opn": "4.0.2", 37 | "pad": "2.1.0", 38 | "promptly": "0.2.1", 39 | "sandboxjs": "5.0.0", 40 | "semver": "5.5.0", 41 | "structured-cli": "1.0.5", 42 | "superagent": "3.8.3", 43 | "superagent-proxy": "2.0.0", 44 | "update-notifier": "2.5.0", 45 | "webtask-bundle": "^3.4.0", 46 | "webtask-runtime": "2.0.0" 47 | }, 48 | "bin": { 49 | "wt": "./bin/wt", 50 | "wt-cli": "./bin/wt" 51 | }, 52 | "homepage": "http://github.com/auth0/wt-cli", 53 | "repository": { 54 | "type": "git", 55 | "url": "git@github.com:auth0/wt-cli.git" 56 | }, 57 | "bugs": { 58 | "url": "http://github.com/auth0/wt-cli/issues" 59 | }, 60 | "devDependencies": { 61 | "eslint": "5.3.0" 62 | }, 63 | "prettier": { 64 | "tabWidth": 4 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sample-webtasks/README.md: -------------------------------------------------------------------------------- 1 | ### Create 2 | 3 | Creates a webtask based on a local file and returns a URL that can be used to execute it. 4 | 5 | ```bash 6 | $ wt create foo.js 7 | ``` 8 | 9 | > Specifying `--watch` modifier will watch for file changes and refresh the webtask 10 | 11 | ### Create from URL 12 | 13 | Creates a webtask that when called it will fetch the code from that public URL and execute it. By default the code is not cached, use `--prod` modifier to get a URL where the code is cached. 14 | 15 | ```bash 16 | $ wt create https://raw.githubusercontent.com/auth0/wt-cli/master/sample-webtasks/html-response.js \ 17 | --name html-response-url 18 | ``` 19 | 20 | ### Logs 21 | 22 | Shows the log streaming of all your webtasks. All `console.log` calls will be available. 23 | 24 | ```bash 25 | $ wt logs 26 | ``` 27 | 28 | ### Secrets 29 | 30 | Create a webtask that depends on a secret (a mongodb connection string). 31 | 32 | ```bash 33 | $ wt create https://raw.githubusercontent.com/auth0/wt-cli/master/sample-webtasks/mongodb.js \ 34 | --name mongo \ 35 | --secret MONGO_URL=mongodb://webtask:supersecret@ds047592.mongolab.com:47592/webtask-examples 36 | ``` 37 | 38 | > Secrets are encrypted with AES256-CBC. This is a real mongodb URL (powered by mongolab), no guarantee that it will work :) 39 | 40 | ### Cron 41 | 42 | Cron a webtask that will run every 10 minutes. 43 | 44 | ```bash 45 | $ wt cron schedule -n mongocron \ 46 | -s MONGO_URL=mongodb://webtask:supersecret@ds047592.mongolab.com:47592/webtask-examples \ 47 | "*/10 * * * *" \ 48 | https://raw.githubusercontent.com/auth0/wt-cli/master/sample-webtasks/mongodb.js 49 | ``` 50 | 51 | ### Express.js 52 | 53 | You can use the [express](https://expressjs.com) framework inside a webtask. Specify `--no-parse` and `--no-merge` modifiers to keep the request raw. 54 | 55 | ```bash 56 | $ wt create express.js --no-parse --no-merge 57 | ``` 58 | 59 | ### Cron history 60 | 61 | Get a history of all the runs of a specific cron. 62 | 63 | ```bash 64 | $ wt cron history mongocron 65 | ``` 66 | 67 | ### Crons 68 | 69 | Get a list of all the crons you have registered. 70 | 71 | ```bash 72 | $ wt cron ls 73 | ``` 74 | -------------------------------------------------------------------------------- /sample-webtasks/attack-cpu.js: -------------------------------------------------------------------------------- 1 | /* attack cpu - look at logs with `wt logs` */ 2 | 3 | module.exports = 4 | function (cb) { 5 | while(true); 6 | } -------------------------------------------------------------------------------- /sample-webtasks/attack-forkbomb.js: -------------------------------------------------------------------------------- 1 | /* fork bomb - look at logs with `wt logs` */ 2 | var spawn = require('child_process').spawn; 3 | 4 | module.exports = function (cb) { 5 | spawn_one(); 6 | 7 | function spawn_one() { 8 | console.log('moaaar spawning') 9 | spawn('node', ['-e', 'setInterval(function () {}, 1000);']); 10 | process.nextTick(spawn_one); 11 | } 12 | } -------------------------------------------------------------------------------- /sample-webtasks/attack-ram.js: -------------------------------------------------------------------------------- 1 | /* attack memory - look at logs with `wt logs` */ 2 | module.exports = function (cb) { 3 | var evil = 'evil'; 4 | more_evil(); 5 | 6 | function more_evil() { 7 | evil += evil; 8 | console.log('Current length: ' + evil.length); 9 | process.nextTick(more_evil); 10 | } 11 | } -------------------------------------------------------------------------------- /sample-webtasks/bundled-webtask/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2017 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /sample-webtasks/bundled-webtask/data.js: -------------------------------------------------------------------------------- 1 | // While the sample is very simple, this could be a full node module with 2 | // whatever code you want 3 | 4 | module.exports = { 5 | external: 'data', 6 | }; -------------------------------------------------------------------------------- /sample-webtasks/bundled-webtask/index.js: -------------------------------------------------------------------------------- 1 | var Express = require('express'); 2 | var Webtask = require('webtask-tools'); 3 | 4 | var app = Express(); 5 | var externalData = require('./data'); 6 | 7 | 8 | module.exports = Webtask.fromExpress(app); 9 | 10 | 11 | app.use(require('body-parser').json()); 12 | 13 | app.get('/', function (req, res) { 14 | res.json({ 15 | externalData: externalData, 16 | query: req.query, 17 | // For demonstration purposes only. Never echo your secrets back like this. 18 | secrets: req.webtaskContext.secrets, 19 | }); 20 | }); 21 | 22 | app.post('/', function (req, res) { 23 | res.json({ 24 | body: req.body, 25 | externalData: externalData, 26 | query: req.query, 27 | // For demonstration purposes only. Never echo your secrets back like this. 28 | secrets: req.webtaskContext.secrets, 29 | }); 30 | }); 31 | 32 | app.get('/async', async (req, res) => { 33 | await new Promise(resolve => setTimeout(resolve, 2000)); 34 | 35 | res.end('OK'); 36 | }); 37 | -------------------------------------------------------------------------------- /sample-webtasks/bundled-webtask/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundled-webtask", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "wt create --bundle --watch ." 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "dependencies": { 12 | "body-parser": "^1.17.1", 13 | "express": "^4.15.2", 14 | "webtask-tools": "^3.2.0" 15 | }, 16 | "license": "ISC" 17 | } 18 | -------------------------------------------------------------------------------- /sample-webtasks/counter.js: -------------------------------------------------------------------------------- 1 | // Try me locally using: wt serve --storage-file counter.json counter.js 2 | 3 | module.exports = function (ctx, cb) { 4 | ctx.storage.get(function (err, data) { 5 | if (err) return cb(err); 6 | if (!data) data = { }; 7 | if (!data.counter) data.counter = 0; 8 | 9 | data.counter++; 10 | 11 | ctx.storage.set(data, function (err) { 12 | if (err) return cb(err); 13 | 14 | cb(null, data); 15 | }); 16 | }); 17 | } -------------------------------------------------------------------------------- /sample-webtasks/csharp.js: -------------------------------------------------------------------------------- 1 | /* Using C# via Edge.js: http://tjanczuk.github.io/edge */ 2 | 3 | module.exports = function (cb) { 4 | require('edge').func(function () {/* 5 | async (dynamic context) => { 6 | return "Hello, world from C#!"; 7 | } 8 | */})(null, cb); 9 | } -------------------------------------------------------------------------------- /sample-webtasks/es6-template-literals.js: -------------------------------------------------------------------------------- 1 | module.exports = function (context, req, res) { 2 | const templateLiteral = `

Howdy, ${context.data.name}!

`; 3 | 4 | res.writeHead(200, { 'Content-Type': 'text/html' }); 5 | res.end(templateLiteral); 6 | } 7 | -------------------------------------------------------------------------------- /sample-webtasks/es6.js: -------------------------------------------------------------------------------- 1 | "use latest"; 2 | 3 | /* Enable latest ESvNext features through babel with "use latest"; 4 | at the top of the script. */ 5 | 6 | module.exports = cb => cb(null, "Welcome to ES6 arrows!"); -------------------------------------------------------------------------------- /sample-webtasks/express.js: -------------------------------------------------------------------------------- 1 | /* express app as a webtask */ 2 | 3 | var Express = require('express'); 4 | var Webtask = require('webtask-tools'); 5 | var app = Express(); 6 | 7 | app.use(require('body-parser').json()); 8 | 9 | // POST 10 | app.post('/sample/path', function (req, res) { 11 | res.json(req.body); 12 | }); 13 | 14 | // GET 15 | app.get('*', function (req, res) { 16 | res.json({ id: 1 }); 17 | }); 18 | 19 | // PUT 20 | app.put('*', function (req, res) { 21 | res.json({ id: 1 }); 22 | }); 23 | 24 | // DELETE 25 | app.delete('*', function (req, res) { 26 | res.json({ id: 1 }) 27 | }); 28 | 29 | // expose this express app as a webtask-compatible function 30 | 31 | module.exports = Webtask.fromExpress(app); 32 | -------------------------------------------------------------------------------- /sample-webtasks/github-tag-hook.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Request = Bluebird.promisifyAll(require('request')); 3 | var _ = require('lodash'); 4 | 5 | var API_URL = 'https://api.github.com'; 6 | var WEB_URL = 'https://github.com'; 7 | var REF = 'refs/heads/master'; 8 | 9 | /** 10 | * Automatically tag new versions via webhook 11 | * 12 | * A webtask that can be used as a Github webhook to automatically tag new 13 | * versions based on changes to the file `package.json`. 14 | * 15 | * Installation instructions: 16 | * 1. Install the webtask cli: `npm install -g wt-cli` 17 | * 2. Create a webtask profile: `wt init` 18 | * 3. Create a Github API token with `repo` access from: https://github.com/settings/tokens/new 19 | * 4. Generate the webhook url, substituting with the one from step #3: `wt create --name auto_tag --secret GITHUB_TOKEN= --prod https://raw.githubusercontent.com/auth0/wt-cli/master/sample-webtasks/github-tag-hook.js` 20 | * 5. Install the webhook with the default settings on your repo by subsituting and , at: https://github.com///settings/hooks/new 21 | * 6. Optionally inspect any errors using the cli: `wt logs` 22 | * 23 | * @webtask_option pb 1 - This webtask requires that the body automatically be parsed 24 | * @webtask_secret GITHUB_TOKEN - A Github access token 25 | */ 26 | module.exports = function (ctx, cb) { 27 | var msg; 28 | var err; 29 | 30 | if (!ctx.body) { 31 | err = new Error('This webtask must be created with the `--parse` flag (`pb` claim)'); 32 | return cb(err); 33 | } 34 | 35 | if (!Array.isArray(ctx.body.commits)) { 36 | err = new Error('Unexpected payload: Missing commits array.'); 37 | return cb(err); 38 | } 39 | 40 | if (!ctx.body.repository) { 41 | err = new Error('Unexpected payload: Missing repository information.'); 42 | return cb(err); 43 | } 44 | var payload = ctx.body; 45 | var affectsPackageJson = _.find(payload.commits, function (commit) { 46 | return commit.modified.indexOf('package.json') >= 0 47 | || commit.added.indexOf('package.json') >= 0; 48 | }); 49 | 50 | if (payload.ref !== REF) { 51 | msg = 'Push event does not affect the ref `' + REF + '`.'; 52 | return cb(null, msg); 53 | } 54 | 55 | if (!affectsPackageJson) { 56 | msg = 'Commits `' + _(payload.commits).pluck('id').join('`, `') 57 | + '` do not affect the file `package.json`.'; 58 | return cb(null, msg); 59 | } 60 | 61 | var headers = { 62 | 'Authorization': 'Bearer ' + ctx.data.GITHUB_TOKEN, 63 | 'User-Agent': 'Webtask Tagger', 64 | }; 65 | 66 | var versionBeforePromise = getVersionFromCommit(payload.before); 67 | var versionAfterPromise = getVersionFromCommit(payload.after); 68 | 69 | Bluebird.join(versionBeforePromise, versionAfterPromise, function (versionBefore, versionAfter) { 70 | return versionBefore !== versionAfter 71 | ? createNewTag(payload.after, versionAfter) 72 | : 'This push did not update the package\'s version'; 73 | }) 74 | .nodeify(cb); 75 | 76 | 77 | // Helper functions 78 | 79 | function getVersionFromCommit(commitSha) { 80 | // If we're dealing with the initial commit, the `before` 81 | // commit sha will be zeroed out. Shortcut and return 0.0.0. 82 | if (commitSha === '0000000000000000000000000000000000000000') { 83 | return Bluebird.resolve('0.0.0'); 84 | } 85 | 86 | var url = WEB_URL + '/' + payload.repository.full_name + '/raw/' + commitSha + '/package.json'; 87 | var promise = Request.getAsync(url); 88 | 89 | return promise 90 | // Because request callbacks have the (err, res, body) signature, 91 | // Bluebird will resolve to a 2-element array like [res, body]. 92 | // We only want the body, at index 1 in the array. 93 | .get(1) 94 | // The body should be a plain String, we want to parse 95 | // it into a javascript object. 96 | .then(JSON.parse) 97 | // Now that our Promise contains the parsed package.json, let's 98 | // pull out the `version`. 99 | .get('version'); 100 | } 101 | 102 | function createNewTag(commitSha, version) { 103 | var now = new Date(); 104 | var url = API_URL + '/repos/' + payload.repository.full_name + '/git/tags'; 105 | var options = { 106 | url: url, 107 | headers: headers, 108 | json: true, 109 | body: { 110 | tag: 'v' + version, 111 | message: 'v' + version, 112 | object: commitSha, 113 | type: 'commit', 114 | tagger: { 115 | name: payload.pusher.name, 116 | email: payload.pusher.email, 117 | date: now.toISOString(), 118 | }, 119 | }, 120 | }; 121 | var promise = Request.postAsync(options); 122 | 123 | return promise 124 | .get(1) 125 | .then(function (tag) { 126 | return createTagRef(tag.sha, tag.tag); 127 | }); 128 | } 129 | 130 | function createTagRef(commitSha, tagName) { 131 | var url = API_URL + '/repos/' + payload.repository.full_name + '/git/refs'; 132 | var options = { 133 | url: url, 134 | headers: headers, 135 | json: true, 136 | body: { 137 | ref: 'refs/tags/' + tagName, 138 | sha: commitSha, 139 | }, 140 | }; 141 | var promise = Request.postAsync(options); 142 | 143 | return promise 144 | .get(1) 145 | .then(function (tagRef) { 146 | return 'Successfully created tag `' + tagName + '`.'; 147 | }); 148 | } 149 | }; -------------------------------------------------------------------------------- /sample-webtasks/google-places-api.js: -------------------------------------------------------------------------------- 1 | "use latest"; 2 | 3 | // Using the request-promise module [https://www.npmjs.com/package/request-promise] 4 | var request = require('request-promise'); 5 | 6 | // Create webtask using wt create -s gpakey= google-places-api 7 | const googlePlacesUrl = 'https://maps.googleapis.com/maps/api/place/nearbysearch/'; 8 | const keyword = 'food'; 9 | const output = 'json'; 10 | 11 | /** 12 | * Google Places API webhook example via webtask 13 | * 14 | * Looks for open food locations within a specified radius 15 | * of a target latitude and longitude. 16 | * 17 | * This uses the Google Place API for web-services found here: https://developers.google.com/places/web-service/search 18 | * 19 | * Setup Guide: 20 | * 1. Install the webtask cli: `npm install -g wt-cli` 21 | * 2. Create the webtask profile: `wt init` or `wt init ` 22 | * 2.a. Check your email and enter verification code if necessary 23 | * 3. Create a Google Places API Key here: https://developers.google.com/places/web-service/get-api-key 24 | * 4. Push your webhook to the server using (while in same directory): `wt create -s gpakey= --name google-places-api google-places-api.js` 25 | * 4.a. Substitute with the Google Places API Key from Step 3 26 | * 5. Test webhook using: `curl -d longitude=100.5017651 -d latitude=13.7563309 -d radius=100 ` 27 | * 5.a. Substitute with the url contained in the response from Step 4 28 | * 5.b. Need curl to be installed. Type (On Ubuntu): `sudo apt-get install -y curl` 29 | * 6. Inspect error/debug using `wt logs` 30 | * 31 | * @webtask_data latitude - Latitude of the center point of the search area (default 0.0) 32 | * @webtask_data longitude - Longitude of the center point of the search area (default 0.0) 33 | * @webtask_data radius - Radius from center point to search in meters (default 100 meters) 34 | * @webtask_secret gpakey - Your Google Places API Key 35 | */ 36 | 37 | module.exports = function(context, cb) { 38 | let lat = context.body.latitude || '0.0'; 39 | let long = context.body.longitude || '0.0'; 40 | 41 | let radius = context.body.radius || '100'; 42 | let location = `${lat},${long}`; 43 | let key = context.secrets.gpakey; 44 | 45 | let request_url = `${googlePlacesUrl}${output}?location=${location}&radius=${radius}&keyword=${keyword}&key=${key}&opennow=true`; 46 | 47 | request(request_url, { json: true }) 48 | .then( function(data) { 49 | if(data && (data.status == 'OK' || data.status == 'ZERO_RESULTS') ) { 50 | let response = { 51 | timestamp: new Date(), 52 | latitude: lat, 53 | longitude: long, 54 | radius: radius, 55 | results: data.results || [] 56 | }; 57 | cb(null, response); 58 | } else { 59 | let error = new Error(`${data.status}${data.error_message ? ':'+data.error_message : ''}`); 60 | return cb(error); 61 | } 62 | }) 63 | .catch( function(error) { 64 | cb(error); 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /sample-webtasks/hello-world.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | function (cb) { 3 | cb(null, 'Hello, world!'); 4 | } -------------------------------------------------------------------------------- /sample-webtasks/html-response.js: -------------------------------------------------------------------------------- 1 | /* More on programming models: https://webtask.io/docs/model */ 2 | 3 | var view = (function view() {/* 4 | 5 | 6 | Welcome to Webtasks 7 | 8 | 9 |

Hello, <%= name %>

10 | 11 | 12 | */}).toString().match(/[^]*\/\*([^]*)\*\/\s*\}$/)[1]; 13 | 14 | module.exports = 15 | function (context, req, res) { 16 | res.writeHead(200, { 'Content-Type': 'text/html' }); 17 | res.end(require('ejs').render(view, { 18 | name: context.data.name || 'Anonymous' 19 | })); 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample-webtasks/httppost.js: -------------------------------------------------------------------------------- 1 | /* 2 | wt create httppost.js 3 | curl --data foo=bar https://webtask.it.auth0.com/api/run/yours/httppost 4 | */ 5 | 6 | module.exports = 7 | function (context, cb) { 8 | cb(null, 'Hello, world! ' + context.body.foo); 9 | } 10 | -------------------------------------------------------------------------------- /sample-webtasks/json-response.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | function (cb) { 3 | cb(null, { 4 | now: new Date(), 5 | flag: true, 6 | name: 'Auth0', 7 | amount: 200, 8 | details: { 9 | history: [1,2,3,4], 10 | average: 2.5 11 | } 12 | }); 13 | } -------------------------------------------------------------------------------- /sample-webtasks/logging.js: -------------------------------------------------------------------------------- 1 | /* `wt logs` -- will stream your logs to the console 2 | More on logging: https://webtask.io/docs/api_logs */ 3 | 4 | module.exports = 5 | function (context, cb) { 6 | console.log('foo') 7 | console.log('request received', context.data); 8 | cb(null, 'Hello, world'); 9 | } -------------------------------------------------------------------------------- /sample-webtasks/mongodb.js: -------------------------------------------------------------------------------- 1 | /* 2 | Connects to mongo. The mongo connection url is specified on webtask creation (and is encrypted) 3 | wt create task -s MONGO_URL=mongodb:// 4 | */ 5 | 6 | var MongoClient = require('mongodb').MongoClient; 7 | var waterfall = require('async').waterfall; 8 | 9 | /** 10 | * @param {secret} MONGO_URL - Mongo database url 11 | */ 12 | module.exports = function(ctx, cb) { 13 | 14 | var MONGO_URL = ctx.data.MONGO_URL; 15 | if (!MONGO_URL) return cb(new Error('MONGO_URL secret is missing')) 16 | 17 | waterfall([ 18 | function connect_to_db(done) { 19 | MongoClient.connect(MONGO_URL, function(err, db) { 20 | if(err) return done(err); 21 | 22 | done(null, db); 23 | }); 24 | }, 25 | function do_something(db, done) { 26 | db 27 | .collection('my-collection') 28 | .insertOne({ msg: 'Hey Mongo!' }, function (err, result) { 29 | if(err) return done(err); 30 | 31 | done(null, result); 32 | }); 33 | } 34 | ], cb); 35 | }; 36 | -------------------------------------------------------------------------------- /sample-webtasks/request.js: -------------------------------------------------------------------------------- 1 | /* Use Node.js modules: https://webtask.io/docs/101 */ 2 | 3 | var request = require('request'); 4 | 5 | module.exports = 6 | function (cb) { 7 | var start = Date.now(); 8 | request.get('https://auth0.com', function (error, res, body) { 9 | if (error) 10 | cb(error); 11 | else 12 | cb(null, { 13 | status: res.statusCode, 14 | length: body.length, 15 | latency: Date.now() - start 16 | }); 17 | }); 18 | } -------------------------------------------------------------------------------- /sample-webtasks/unverified-emails.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Boom = require('boom'); 3 | var Request = Bluebird.promisifyAll(require('request')); 4 | var _ = require('lodash'); 5 | 6 | 7 | /** 8 | * Webtask that will query the Auth0 user search api for users who have not yet verified 9 | * their email address, then trigger a reminder to do so for those users, then email 10 | * the administrator a list of users so-notified. 11 | * 12 | * @param {string} AUTH0_TENANT - The Auth0 tenant for which you would like to run the job. 13 | * @param {secret} AUTH0_JWT - An Auth0 jwt obtained on https://auth0.com/docs/api/v2 after adding the read:users and update:users scopes. 14 | */ 15 | module.exports = function (context, cb) { 16 | return Bluebird.try(createScopedRequest, [context]) 17 | .then(function (request) { 18 | return Bluebird 19 | .bind({scopedRequest: request}) 20 | .then(getUsersWithUnconfirmedEmails) 21 | .map(sendVerificationEmail) 22 | .then(_) 23 | .call('pluck', 'email'); 24 | }) 25 | .nodeify(cb); 26 | }; 27 | 28 | function createScopedRequest (context) { 29 | var tenant = context.data.AUTH0_TENANT; 30 | var jwt = context.data.AUTH0_JWT; 31 | 32 | if (!tenant) throw Boom.preconditionFailed('Missing parameter AUTH0_TENANT'); 33 | if (!jwt) throw Boom.preconditionFailed('Missing secret AUTH0_JWT'); 34 | 35 | return Bluebird.promisifyAll(Request.defaults({ 36 | baseUrl: 'https://' + tenant + '.auth0.com/api/v2/', 37 | headers: { 38 | 'Authorization': 'Bearer ' + jwt, 39 | }, 40 | json: true, 41 | })); 42 | } 43 | 44 | 45 | function getUsersWithUnconfirmedEmails () { 46 | return this.scopedRequest.getAsync('/users', { 47 | qs: { 48 | q: 'email_verified:false', 49 | search_engine: 'v2', 50 | }, 51 | }) 52 | .spread(checkResponse); 53 | } 54 | 55 | function sendVerificationEmail (user) { 56 | return this.scopedRequest.postAsync('/jobs/verification-email', { 57 | json: { 58 | user_id: user.user_id 59 | }, 60 | }) 61 | .spread(checkResponse) 62 | .return(user); 63 | } 64 | 65 | function checkResponse (res, body) { 66 | if (res.statusCode >= 300) { 67 | console.log('Unexpected response from url `' + res.url + '`:', body); 68 | 69 | throw new Boom((body && body.message) || 'Unexpected response from the server', {statusCode: res.statusCode, data: body}); 70 | } 71 | 72 | return body; 73 | } -------------------------------------------------------------------------------- /sample-webtasks/url-query-parameter.js: -------------------------------------------------------------------------------- 1 | /* URL query parameters are passed through context.data */ 2 | 3 | module.exports = 4 | function (context, cb) { 5 | cb(null, 'Hello, ' + (context.data.name || 'Anonymous')); 6 | } -------------------------------------------------------------------------------- /samples/pkce.js: -------------------------------------------------------------------------------- 1 | // Perform OAuth2 PKCE flow to obtain access and refresh tokens to webtask.io 2 | 3 | const Crypto = require('crypto'); 4 | const Url = require('url'); 5 | const Http = require('http'); 6 | const Superagent = require('superagent'); 7 | 8 | function getAccessToken() { 9 | const codeVerifier = base64URLEncode(Crypto.randomBytes(16)); 10 | const port = 8722 + Math.floor(6 * Math.random()); 11 | const redirectUri = `http://127.0.0.1:${port}`; 12 | const clientId = '2WvfzGDRwAdovNqHiLY13kAvUDarn4NG'; 13 | 14 | // Start local server to receive the callback from the authorization server 15 | Http.createServer((req, res) => { 16 | return processRedirectCallback(req, (error, data) => { 17 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 18 | let msg = error 19 | ? `Authentication failed: ${error.message}` 20 | : `Authentication successful:\n\n${JSON.stringify(data, null, 2)}`; 21 | res.end(msg); 22 | console.log(); 23 | console.log(msg); 24 | process.exit(error ? 1 : 0); 25 | }); 26 | }).listen(port); 27 | 28 | // Create PKCE authorization URL 29 | let loginUrl = Url.parse('https://webtask.auth0.com/authorize', true); 30 | loginUrl.query = { 31 | redirect_uri: redirectUri, 32 | audience: 'https://sandbox.auth0-extend.com', 33 | response_type: 'code', 34 | client_id: clientId, 35 | scope: 'openid offline_access', 36 | code_challenge: base64URLEncode(Crypto.createHash('sha256').update(codeVerifier).digest()), 37 | code_challenge_method: 'S256', 38 | }; 39 | loginUrl = Url.format(loginUrl); 40 | 41 | return console.log(`Navigate to the following URL in your browser to obtain webtask.io access token:\n\n${loginUrl}`); 42 | 43 | function base64URLEncode(str) { 44 | return str.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 45 | } 46 | 47 | // Process redirect from authorization server to get authorization code 48 | function processRedirectCallback(req, cb) { 49 | var url = Url.parse(req.url, true); 50 | if (req.method !== 'GET' || url.pathname !== '/') { 51 | return cb(new Error(`Invalid redirect from authorization server: ${req.method} ${req.url}`)); 52 | } 53 | if (url.query.error) { 54 | return cb(new Error(`${url.query.error}: ${url.query.error_description}`)); 55 | } 56 | if (!url.query.code) { 57 | return cb(new Error(`Authorization server response does not specify authorization code: ${req.url}.`)); 58 | } 59 | 60 | return exchangeAuthorizationCode(url.query.code, cb); 61 | } 62 | 63 | // Exchange authorization code for access token using PKCE 64 | function exchangeAuthorizationCode(code, cb) { 65 | return Superagent 66 | .post('https://webtask.auth0.com/oauth/token') 67 | .send({ 68 | grant_type: 'authorization_code', 69 | client_id: clientId, 70 | code, 71 | code_verifier: codeVerifier, 72 | redirect_uri: redirectUri 73 | }) 74 | .end((e, r) => { 75 | if (e) return cb(new Error(`Authentication failed. Unable to obtian access token: ${e.message}.`)); 76 | return cb(null, r.body) 77 | }); 78 | } 79 | } 80 | 81 | return getAccessToken(); 82 | --------------------------------------------------------------------------------