From 731716efe1df6ca00f23f71cad1fe64695533e96 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 13 May 2016 14:33:30 -0700 Subject: [PATCH 1/4] delete dead code This code makes it look like there are more codepaths that care about $DEPLOY_HOSTNAME than there actually are. --- tools/meteor-services/config.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tools/meteor-services/config.js b/tools/meteor-services/config.js index fc2ee2f051..63d526d09a 100644 --- a/tools/meteor-services/config.js +++ b/tools/meteor-services/config.js @@ -249,18 +249,6 @@ _.extend(exports, { return getUniverse(); }, - getDeployHostname: function () { - return process.env.DEPLOY_HOSTNAME || "meteor.com"; - }, - - getFullAppName: function(appName) { - var domain = process.env.DEPLOY_DOMAIN || this.getDeployHostname(); - if (appName.indexOf(".") === -1) { - return appName + "." + domain; - } - return appName; - }, - // Deploy URL for MDG free hosting, eg 'https://deploy.meteor.com'. getDeployUrl: function () { var host; From 2b6b02439dfb9022a1f9d4715801f8cfe0e6417e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 13 May 2016 15:02:49 -0700 Subject: [PATCH 2/4] destroy universe "universe" was an old attempt to allow you to run various MDG servers (Meteor Developer Accounts, the package server, an old version of the Galaxy deploy server, etc) on localhost and configure the tool to talk to it not via a bunch of environment variables but via a file called "universe" at the root of your checkout. Nobody uses this (and most of the URLs have environment variables for them anyway). Simplify the code by removing this entirely. Also remove some more dead code, and a test that claims it only runs if you have universe set up. --- tools/cli/commands.js | 6 - tools/meteor-services/auth.js | 5 - tools/meteor-services/config.js | 220 ++++--------------------------- tools/tests/organizations.js | 51 ------- tools/tool-testing/test-utils.js | 5 - 5 files changed, 26 insertions(+), 261 deletions(-) diff --git a/tools/cli/commands.js b/tools/cli/commands.js index c5f8bbfdd0..38a41e83b1 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -1145,7 +1145,6 @@ to this command.`); } else { // remote mode var site = qualifySitename(options.args[0]); - config.printUniverseBanner(); mongoUrl = deploy.temporaryMongoUrl(site); usedMeteorAccount = true; @@ -1240,7 +1239,6 @@ main.registerCommand({ catalogRefresh: new catalog.Refresh.Never() }, function (options, {rawOptions}) { var site = options.args[0]; - config.printUniverseBanner(); if (options.delete) { return deploy.deleteApp(site); @@ -1358,7 +1356,6 @@ main.registerCommand({ return 1; } - config.printUniverseBanner(); auth.pollForRegistrationCompletion(); var site = qualifySitename(options.args[0]); @@ -1390,7 +1387,6 @@ main.registerCommand({ maxArgs: 1, catalogRefresh: new catalog.Refresh.Never() }, function (options) { - config.printUniverseBanner(); auth.pollForRegistrationCompletion(); var site = qualifySitename(options.args[0]); @@ -1956,8 +1952,6 @@ main.registerCommand({ throw new main.ShowUsage; } - config.printUniverseBanner(); - var username = options.add || options.remove; var conn = loggedInAccountsConnectionOrPrompt( diff --git a/tools/meteor-services/auth.js b/tools/meteor-services/auth.js index 8fd4af9953..2d2c043a6e 100644 --- a/tools/meteor-services/auth.js +++ b/tools/meteor-services/auth.js @@ -555,8 +555,6 @@ exports.doInteractivePasswordLogin = doInteractivePasswordLogin; exports.loginCommand = withAccountsConnection(function (options, connection) { - config.printUniverseBanner(); - var data = readSessionData(); if (! getSession(data, config.getAccountsDomain()).token || @@ -593,8 +591,6 @@ exports.loginCommand = withAccountsConnection(function (options, }); exports.logoutCommand = function (options) { - config.printUniverseBanner(); - var data = readSessionData(); var wasLoggedIn = !! loggedIn(data); logOutAllSessions(data); @@ -686,7 +682,6 @@ exports.registrationUrl = function () { }; exports.whoAmICommand = function (options) { - config.printUniverseBanner(); auth.pollForRegistrationCompletion(); var data = readSessionData(); diff --git a/tools/meteor-services/config.js b/tools/meteor-services/config.js index 63d526d09a..14d5679113 100644 --- a/tools/meteor-services/config.js +++ b/tools/meteor-services/config.js @@ -8,179 +8,66 @@ var tropohouse = require('../packaging/tropohouse.js'); // deploying apps to the MDG free hosting sandbox, publishing packages, // getting an ssh access to a build farm. These functions need // configuration. -// -// The idea is that eventually, the `meteor` will take only one -// configuration parameter, the "universe" it is talking to, which -// defaults to "www.meteor.com". In a git checkout it can be set by -// creating a file at the root of the checkout called "universe" that -// contains the name of the universe you wish to use. Then, all other -// needed configuration is derived from the universe name. -// -// We're not quite there yet though: -// - When developing locally, you may need to set DISCOVERY_PORT (see -// getDiscoveryPort below) -// - DEPLOY_HOSTNAME can still be set to override classic-style -// deploys -// - The update/warehouse system hasn't been touched and still has its -// hardcoded URLs for now (update.meteor.com and -// warehouse.meteor.com). Really, it's debatable whether these -// should (necessarily) change when you change your universe name. - -var universe; -var getUniverse = function () { - if (! universe) { - universe = "www.meteor.com"; - - if (files.inCheckout()) { - var p = files.pathJoin(files.getCurrentToolsDir(), 'universe'); - if (files.exists(p)) { - universe = files.readFile(p, 'utf8').trim(); - } - } - } - - return universe; -}; - -var isLocalUniverse = function () { - return !! getUniverse().match(/^localhost(:([\d]+))?$/); -}; - -var localhostOffset = function (portOffset) { - var match = getUniverse().match(/^localhost(:([\d]+))?$/); - if (! match) { - throw new Error("not a local universe?"); - } - return "localhost:" + (parseInt(match[2] || "80") + portOffset); -}; - -var getAuthServiceHost = function () { - if (! isLocalUniverse()) { - return universe; - } else { - // Special case for local development. Point - // $METEOR_CHECKOUT/universe at the place where you are running - // frontpage (eg, localhost:3000), and run the accounts server ten - // port numbers higher. Like so: - // cd meteor-accounts - // ROOT_URL=http://localhost:3010/auth curmeteor -p 3010 - return localhostOffset(10); - } -}; // Given a hostname, add "http://" or "https://" as // appropriate. (localhost gets http; anything else is always https.) -var addScheme = function (host) { - if (host.match(/^localhost(:\d+)?$/)) { - return "http://" + host; +var addScheme = function (hostOrURL) { + if (hostOrURL.match(/^http/)) { + return hostOrURL; + } else if (hostOrURL.match(/^localhost(:\d+)?$/)) { + return "http://" + hostOrURL; } else { - return "https://" + host; + return "https://" + hostOrURL; } }; var config = exports; _.extend(exports, { - // True if this the production universe (www.meteor.com) - isProduction: function () { - return getUniverse() === "www.meteor.com"; - }, - - // The current universe name. Should be used for cosmetic purposes - // only (displaying to the user). If you want to programmatically - // derive configuration from it, add a new method to this file. - getUniverse: function () { - return getUniverse(); - }, - - // Base URL for Meteor Accounts OAuth services, typically - // "https://www.meteor.com/oauth2". Endpoints include /authorize and - // /token. + // Base URL for Meteor Accounts OAuth services. Endpoints include /authorize + // and /token. getOauthUrl: function () { - return addScheme(getAuthServiceHost()) + "/oauth2"; + return "https://www.meteor.com/oauth2"; }, - // Base URL for Meteor Accounts API, typically - // "https://www.meteor.com/api/v1". Endpoints include '/login' and + // Base URL for Meteor Accounts API. Endpoints include '/login' and // '/logoutById'. getAccountsApiUrl: function () { - return addScheme(getAuthServiceHost()) + "/api/v1"; + return "https://www.meteor.com/api/v1"; }, - // URL for the DDP interface to Meteor Accounts, typically - // "https://www.meteor.com/auth". (Really should be a ddp:// URL -- - // we'll get there soon enough.) + // URL for the DDP interface to Meteor Accounts. getAuthDDPUrl: function () { - return addScheme(getAuthServiceHost()) + "/auth"; + return "https://www.meteor.com/auth"; }, // URL for the DDP interface to the meteor build farm, typically // "https://build.meteor.com". getBuildFarmUrl: function () { - if (process.env.METEOR_BUILD_FARM_URL) { - return process.env.METEOR_BUILD_FARM_URL; - } - var host = config.getBuildFarmDomain(); - - return addScheme(host); + return process.env.METEOR_BUILD_FARM_URL || "https://build.meteor.com"; }, getBuildFarmDomain: function () { - if (process.env.METEOR_BUILD_FARM_URL) { - var parsed = url.parse(process.env.METEOR_BUILD_FARM_URL); - return parsed.host; - } else { - return getUniverse().replace(/^www\./, 'build.'); - } + return url.parse(config.getBuildFarmUrl()).host; }, // URL for the DDP interface to the package server, typically - // "https://packages.meteor.com". (Really should be a ddp:// URL -- - // we'll get there soon enough.) - // - // When running everything locally, run the package server at the - // base universe port number (that is, the Meteor Accounts port - // number) plus 20. + // "https://packages.meteor.com". getPackageServerUrl: function () { - if (process.env.METEOR_PACKAGE_SERVER_URL) { - return process.env.METEOR_PACKAGE_SERVER_URL; - } - var host = config.getPackageServerDomain(); - - return addScheme(host); + return process.env.METEOR_PACKAGE_SERVER_URL || + "https://packages.meteor.com"; }, getPackageServerDomain: function () { - if (isLocalUniverse()) { - return localhostOffset(20); - } else { - if (process.env.METEOR_PACKAGE_SERVER_URL) { - var parsed = url.parse(process.env.METEOR_PACKAGE_SERVER_URL); - return parsed.host; - } else { - return getUniverse().replace(/^www\./, 'packages.'); - } - } + return url.parse(config.getPackageServerUrl()).host; }, getPackageStatsServerUrl: function () { - if (process.env.METEOR_PACKAGE_STATS_SERVER_URL) { - return process.env.METEOR_PACKAGE_STATS_SERVER_URL; - } - - var host = config.getPackageStatsServerDomain(); - return addScheme(host); + return process.env.METEOR_PACKAGE_STATS_SERVER_URL || + "https://activity.meteor.com"; }, getPackageStatsServerDomain: function () { - if (process.env.METEOR_PACKAGE_STATS_SERVER_URL) { - return url.parse(process.env.METEOR_PACKAGE_STATS_SERVER_URL).hostname; - } - - if (isLocalUniverse()) { - return localhostOffset(30); - } else { - return getUniverse().replace(/^www\./, 'activity.'); - } + return url.parse(config.getPackageStatsServerUrl()).host; }, // Note: this is NOT guaranteed to return a distinct prefix for every @@ -246,44 +133,16 @@ _.extend(exports, { // use. This is used as a key for storing your Meteor Accounts // login token. getAccountsDomain: function () { - return getUniverse(); + return "www.meteor.com"; }, - // Deploy URL for MDG free hosting, eg 'https://deploy.meteor.com'. + // Deploy URL for Galaxy deployment getDeployUrl: function () { - var host; - - // Support the old DEPLOY_HOSTNAME environment variable for a - // while longer. Soon, let's remove this in favor of the universe - // scheme. if (process.env.DEPLOY_HOSTNAME) { - host = process.env.DEPLOY_HOSTNAME; - if (host.match(/^http/)) { - // allow it to contain a URL scheme - return host; - } + return addScheme(process.env.DEPLOY_HOSTNAME); } else { - // Otherwise, base it on the universe. - if (isLocalUniverse()) { - throw new Error("local development of deploy server not supported"); - } else { - host = getUniverse().replace(/^www\./, 'deploy.'); - } + return "https://deploy.meteor.com"; } - - return addScheme(host); - }, - - // URL from which the update manifest may be fetched, eg - // 'https://update.meteor.com/manifest.json' - getUpdateManifestUrl: function () { - if (isLocalUniverse()) { - // localhost can't run the manifest server - u = "www.meteor.com"; - } - var host = getUniverse().replace(/^www\./, 'update.'); - - return addScheme(host) + "/manifest.json"; }, // Path to file that contains our credentials for any services that @@ -293,32 +152,5 @@ _.extend(exports, { // METEOR_SESSION_FILE is for automated testing purposes only. return process.env.METEOR_SESSION_FILE || files.pathJoin(files.getHomeDir(), '.meteorsession'); - }, - - // Port to use when querying URLs for the deploy server that backs - // them, and for querying oauth clients for their oauth information - // (so we can log into them). - // - // In production this should always be 443 (we *must* - // cryptographically authenticate the server answering the query), - // but this can be inconvenient for local development since 443 is a - // privileged port, so you can set DISCOVERY_PORT to override. (A - // better solution would probably be to spin up a local VM.) - getDiscoveryPort: function () { - if (process.env.DISCOVERY_PORT) { - return parseInt(process.env.DISCOVERY_PORT); - } else { - return 443; - } - }, - - // It's easy to forget that you're in an alternate universe (and - // that that is the reason you're not seeing your deploys). If not - // in production mode, print a quick hint about the universe you're - // in. - printUniverseBanner: function () { - if (! config.isProduction()) { - process.stderr.write('[Universe: ' + config.getUniverse() + ']\n'); - } } }); diff --git a/tools/tests/organizations.js b/tools/tests/organizations.js index c6ab6212a8..820a9491d3 100644 --- a/tools/tests/organizations.js +++ b/tools/tests/organizations.js @@ -36,54 +36,3 @@ selftest.define("organizations - logged out", function () { run.stop(); }); - -// For now, this test only runs from checkout with a universe file -// pointing to a testing meteor-accounts server (e.g. one deployed with -// Meteor.settings.testing = true). -selftest.define("organizations", ["net", "slow", "checkout"], function () { - var s = new Sandbox; - - testUtils.login(s, "test", "testtest"); - - // Create an organization for the test. - var orgName = testUtils.createOrganization("test", "testtest"); - - // Add a nonexistent user. - var run = s.run("admin", "members", - orgName, "--add", testUtils.randomString(15)); - run.waitSecs(commandTimeoutSecs); - run.matchErr("user does not exist"); - run.expectExit(1); - - // Add a user to a nonexistent org. - run = s.run("admin", "members", - testUtils.randomString(15), "--add", "testtest"); - run.waitSecs(commandTimeoutSecs); - run.matchErr("Organization does not exist"); - run.expectExit(1); - - // Add a real user to a real org. - run = s.run("admin", "members", orgName, "--add", "testtest"); - run.waitSecs(commandTimeoutSecs); - run.match("testtest added to organization " + orgName); - run.expectExit(0); - - // Try to show a nonexistent organization. - run = s.run("admin", "members", testUtils.randomString(15)); - run.waitSecs(commandTimeoutSecs); - run.matchErr("Organization does not exist"); - run.expectExit(1); - - // 'show-organization' should show the right members, and - // 'list-organization' should show that 'test' is a member. - run = s.run("admin", "members", orgName); - run.waitSecs(commandTimeoutSecs); - run.read("test\ntesttest\n"); - run.expectExit(0); - run = s.run("admin", "list-organizations"); - run.waitSecs(commandTimeoutSecs); - run.match(orgName + "\n"); - run.expectExit(0); - - testUtils.logout(s); -}); diff --git a/tools/tool-testing/test-utils.js b/tools/tool-testing/test-utils.js index 46d78265f7..7f5f18744e 100644 --- a/tools/tool-testing/test-utils.js +++ b/tools/tool-testing/test-utils.js @@ -48,11 +48,6 @@ exports.logout = function (s) { run.expectExit(0); }; -exports.getUserId = function (s) { - var data = JSON.parse(s.readSessionFile()); - return data.sessions[config.getUniverse()].userId; -}; - var registrationUrlRegexp = /https:\/\/www\.meteor\.com\/setPassword\?([a-zA-Z0-9\+\/]+)/; exports.registrationUrlRegexp = registrationUrlRegexp; From 6619d9bead271d1ea4db433de64bb79b10c6551e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 13 May 2016 15:37:56 -0700 Subject: [PATCH 3/4] remove unnecessary log --- tools/meteor-services/deploy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/meteor-services/deploy.js b/tools/meteor-services/deploy.js index 6f41940418..6673442ab9 100644 --- a/tools/meteor-services/deploy.js +++ b/tools/meteor-services/deploy.js @@ -466,7 +466,6 @@ var bundleAndDeploy = function (options) { dns.resolve(hostname, 'CNAME', function (err, cnames) { if (err || cnames[0] !== 'origin.meteor.com') { dns.resolve(hostname, 'A', function (err, addresses) { - console.log('and here') if (err || addresses[0] !== '107.22.210.133') { Console.info('-------------'); Console.info( From 26584165e5b242f00599b2559379b63457f666c1 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 13 May 2016 18:03:29 -0700 Subject: [PATCH 4/4] discover deploy server and change default Update the default deploy server from deploy.meteor.com (which no longer exists) to galaxy.meteor.com. However, if your app's DNS is already pointed at Galaxy, automatically discover the deploy server's URL. See meteor/amsterdam#305 --- tools/meteor-services/config.js | 21 ------- tools/meteor-services/deploy.js | 107 +++++++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/tools/meteor-services/config.js b/tools/meteor-services/config.js index 14d5679113..ffda20a6a3 100644 --- a/tools/meteor-services/config.js +++ b/tools/meteor-services/config.js @@ -9,18 +9,6 @@ var tropohouse = require('../packaging/tropohouse.js'); // getting an ssh access to a build farm. These functions need // configuration. -// Given a hostname, add "http://" or "https://" as -// appropriate. (localhost gets http; anything else is always https.) -var addScheme = function (hostOrURL) { - if (hostOrURL.match(/^http/)) { - return hostOrURL; - } else if (hostOrURL.match(/^localhost(:\d+)?$/)) { - return "http://" + hostOrURL; - } else { - return "https://" + hostOrURL; - } -}; - var config = exports; _.extend(exports, { // Base URL for Meteor Accounts OAuth services. Endpoints include /authorize @@ -136,15 +124,6 @@ _.extend(exports, { return "www.meteor.com"; }, - // Deploy URL for Galaxy deployment - getDeployUrl: function () { - if (process.env.DEPLOY_HOSTNAME) { - return addScheme(process.env.DEPLOY_HOSTNAME); - } else { - return "https://deploy.meteor.com"; - } - }, - // Path to file that contains our credentials for any services that // we're logged in to. Typically .meteorsession in the user's home // directory. diff --git a/tools/meteor-services/deploy.js b/tools/meteor-services/deploy.js index 6673442ab9..f582036c54 100644 --- a/tools/meteor-services/deploy.js +++ b/tools/meteor-services/deploy.js @@ -42,6 +42,8 @@ const CAPABILITIES = ['showDeployMessages', 'canTransferAuthorization']; // - bodyStream: if provided, a stream to use as the request body // - any other parameters accepted by the node 'request' module, for example // 'qs' to set query string parameters +// - printDeployURL: provided if we should show the deploy URL; set this +// for the first RPC of any user command // // Waits until server responds, then returns an object with the // following keys: @@ -66,10 +68,16 @@ var deployRpc = function (options) { } options.qs = _.extend({}, options.qs, {capabilities: CAPABILITIES}); + const deployURLBase = getDeployURL(options.site).await(); + + if (options.printDeployURL) { + Console.info("Talking to Galaxy servers at " + deployURLBase); + } + // XXX: Reintroduce progress for upload try { var result = httpHelpers.request(_.extend(options, { - url: config.getDeployUrl() + '/' + options.operation + + url: deployURLBase + '/' + options.operation + (options.site ? ('/' + options.site) : ''), method: options.method || 'GET', bodyStream: options.bodyStream, @@ -148,7 +156,8 @@ var authedRpc = function (options) { operation: 'info', site: rpcOptions.site, expectPayload: [], - qs: options.qs + qs: options.qs, + printDeployURL: options.printDeployURL }); if (infoResult.statusCode === 401 && rpcOptions.promptIfAuthFails) { @@ -374,7 +383,8 @@ var bundleAndDeploy = function (options) { site: site, preflight: true, promptIfAuthFails: promptIfAuthFails, - qs: options.rawOptions + qs: options.rawOptions, + printDeployURL: true }); if (preflight.errorMessage) { @@ -493,7 +503,8 @@ var deleteApp = function (site) { method: 'DELETE', operation: 'deploy', site: site, - promptIfAuthFails: true + promptIfAuthFails: true, + printDeployURL: true }); if (result.errorMessage) { @@ -518,7 +529,8 @@ var checkAuthThenSendRpc = function (site, operation, what) { operation: operation, site: site, preflight: true, - promptIfAuthFails: true + promptIfAuthFails: true, + printDeployURL: true }); if (preflight.errorMessage) { @@ -624,7 +636,8 @@ var listAuthorized = function (site) { var result = deployRpc({ operation: 'info', site: site, - expectPayload: [] + expectPayload: [], + printDeployURL: true }); if (result.errorMessage) { Console.error("Couldn't get authorized users list: " + result.errorMessage); @@ -675,7 +688,8 @@ var changeAuthorized = function (site, action, username) { operation: 'authorized', site: site, qs: {[action]: username}, - promptIfAuthFails: true + promptIfAuthFails: true, + printDeployURL: true }); if (result.errorMessage) { @@ -704,7 +718,8 @@ var claim = function (site) { // straight away (at a cost of an extra REST call) var infoResult = deployRpc({ operation: 'info', - site: site + site: site, + printDeployURL: true }); if (infoResult.statusCode === 404) { Console.error( @@ -799,6 +814,82 @@ var listSites = function () { return 0; }; +// Given a hostname, add "http://" or "https://" as +// appropriate. (localhost gets http; anything else is always https.) +function addScheme(hostOrURL) { + if (hostOrURL.match(/^http/)) { + return hostOrURL; + } else if (hostOrURL.match(/^localhost(:\d+)?$/)) { + return "http://" + hostOrURL; + } else { + return "https://" + hostOrURL; + } +}; + +// Maps from "site" to Promise, so we don't have to re-ping on each +// RPC (even if the calls to getDeployURL overlap). +const galaxyDiscoveryCache = new Map; + +// getDeployURL returns the a Promise for the base deploy URL for the given app. +// "app" may be falsey for certain RPCs (eg meteor list-sites). +function getDeployURL(site) { + // Always trust explicitly configuration via env. + if (process.env.DEPLOY_HOSTNAME) { + return Promise.resolve(addScheme(process.env.DEPLOY_HOSTNAME)); + } + + const defaultURL = "https://galaxy.meteor.com"; + + // No site? Just use the default. + if (!site) { + return Promise.resolve(defaultURL); + } + + // If we have a site, we can try to do Galaxy discovery. + + // Do we already have an answer? + if (galaxyDiscoveryCache.has(site)) { + return galaxyDiscoveryCache.get(site); + } + + // Otherwise, try https first, then http, then just use the default. + const p = discoverGalaxy(site, "https") + .catch(() => discoverGalaxy(site, "http")) + .catch(() => defaultURL); + galaxyDiscoveryCache.set(site, p); + return p; +} + +// discoverGalaxy returns the URL to use for Galaxy discovery, or an error if it +// couldn't be fetched. +async function discoverGalaxy(site, scheme) { + const discoveryURL = + scheme + "://" + site + "/.well-known/meteor/deploy-url"; + // If httpHelpers.request throws, the returned Promise will reject, which is + // fine. + const { response, body } = httpHelpers.request({ + url: discoveryURL, + json: true, + strictSSL: true, + // We don't want to be confused by, eg, a non-Galaxy-hosted site which + // redirects to a Galaxy-hosted site. + followRedirect: false + }); + if (response.statusCode !== 200) { + throw new Error("bad status code: " + response.statusCode); + } + if (!body) { + throw new Error("response had no body"); + } + if (body.galaxyDiscoveryVersion !== "galaxy-1") { + throw new Error( + "unexpected galaxyDiscoveryVersion: " + body.galaxyDiscoveryVersion); + } + if (!_.has(body, "deployURL")) { + throw new Error("no deployURL"); + } + return body.deployURL; +} exports.bundleAndDeploy = bundleAndDeploy; exports.deleteApp = deleteApp;