"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const errors_1 = require("@oclif/errors"); const netrc_parser_1 = tslib_1.__importDefault(require("netrc-parser")); const url = tslib_1.__importStar(require("url")); const deps_1 = tslib_1.__importDefault(require("./deps")); const login_1 = require("./login"); const vars_1 = require("./vars"); class HerokuAPIError extends errors_1.CLIError { constructor(httpError) { if (!httpError) throw new Error('invalid error'); let options = httpError.body; if (!options || !options.message) throw httpError; let info = []; if (options.id) info.push(`Error ID: ${options.id}`); if (options.app && options.app.name) info.push(`App: ${options.app.name}`); if (options.url) info.push(`See ${options.url} for more information.`); if (info.length) super([options.message, ''].concat(info).join('\n')); else super(options.message); this.http = httpError; this.body = options; } } exports.HerokuAPIError = HerokuAPIError; class APIClient { constructor(config, options = {}) { this.config = config; this.options = options; this._login = new login_1.Login(this.config, this); this.config = config; if (options.required === undefined) options.required = true; options.preauth = options.preauth !== false; this.options = options; let apiUrl = url.URL ? new url.URL(vars_1.vars.apiUrl) : url.parse(vars_1.vars.apiUrl); let envHeaders = JSON.parse(process.env.HEROKU_HEADERS || '{}'); this.preauthPromises = {}; let self = this; const opts = { host: apiUrl.host, headers: Object.assign({ accept: 'application/vnd.heroku+json; version=3', 'user-agent': `heroku-cli/${self.config.version} ${self.config.platform}` }, envHeaders), }; this.http = class APIHTTPClient extends deps_1.default.HTTP.HTTP.create(opts) { static async twoFactorRetry(err, url, opts = {}, retries = 3) { const app = err.body.app ? err.body.app.name : null; if (!app || !options.preauth) { opts.headers = opts.headers || {}; opts.headers['Heroku-Two-Factor-Code'] = await self.twoFactorPrompt(); return this.request(url, opts, retries); } else { // if multiple requests are run in parallel for the same app, we should // only preauth for the first so save the fact we already preauthed if (!self.preauthPromises[app]) { self.preauthPromises[app] = self.twoFactorPrompt().then((factor) => self.preauth(app, factor)); } await self.preauthPromises[app]; return this.request(url, opts, retries); } } static async request(url, opts = {}, retries = 3) { opts.headers = opts.headers || {}; if (!Object.keys(opts.headers).find(h => h.toLowerCase() === 'authorization')) { opts.headers.authorization = `Bearer ${self.auth}`; } retries--; try { return await super.request(url, opts); } catch (err) { if (!(err instanceof deps_1.default.HTTP.HTTPError)) throw err; if (retries > 0) { if (opts.retryAuth !== false && err.http.statusCode === 401 && err.body.id === 'unauthorized') { if (!self.authPromise) self.authPromise = self.login(); await self.authPromise; opts.headers.authorization = `Bearer ${self.auth}`; return this.request(url, opts, retries); } if (err.http.statusCode === 403 && err.body.id === 'two_factor') { return this.twoFactorRetry(err, url, opts, retries); } } throw new HerokuAPIError(err); } } }; } get twoFactorMutex() { if (!this._twoFactorMutex) { this._twoFactorMutex = new deps_1.default.Mutex(); } return this._twoFactorMutex; } get auth() { if (!this._auth) { if (process.env.HEROKU_API_TOKEN && !process.env.HEROKU_API_KEY) deps_1.default.cli.warn('HEROKU_API_TOKEN is set but you probably meant HEROKU_API_KEY'); this._auth = process.env.HEROKU_API_KEY; if (!this._auth) { deps_1.default.netrc.loadSync(); this._auth = deps_1.default.netrc.machines[vars_1.vars.apiHost] && deps_1.default.netrc.machines[vars_1.vars.apiHost].password; } } return this._auth; } set auth(token) { delete this.authPromise; this._auth = token; } twoFactorPrompt() { deps_1.default.yubikey.enable(); return this.twoFactorMutex.synchronize(async () => { try { let factor = await deps_1.default.cli.prompt('Two-factor code', { type: 'mask' }); deps_1.default.yubikey.disable(); return factor; } catch (err) { deps_1.default.yubikey.disable(); throw err; } }); } preauth(app, factor) { return this.put(`/apps/${app}/pre-authorizations`, { headers: { 'Heroku-Two-Factor-Code': factor }, }); } get(url, options = {}) { return this.http.get(url, options); } post(url, options = {}) { return this.http.post(url, options); } put(url, options = {}) { return this.http.put(url, options); } patch(url, options = {}) { return this.http.patch(url, options); } delete(url, options = {}) { return this.http.delete(url, options); } stream(url, options = {}) { return this.http.stream(url, options); } request(url, options = {}) { return this.http.request(url, options); } login(opts = {}) { return this._login.login(opts); } async logout() { try { await this._login.logout(); } catch (err) { errors_1.warn(err); } delete netrc_parser_1.default.machines['api.heroku.com']; delete netrc_parser_1.default.machines['git.heroku.com']; await netrc_parser_1.default.save(); } get defaults() { return this.http.defaults; } } exports.APIClient = APIClient;