Skip to content

Commit

Permalink
add lib for testing because yarn link not working
Browse files Browse the repository at this point in the history
  • Loading branch information
ryandagg committed Oct 11, 2023
1 parent 8026b15 commit d37e880
Show file tree
Hide file tree
Showing 35 changed files with 1,296 additions and 2 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
*-debug.log
*-error.log
/lib
/node_modules
/tmp
lib/
node_modules/
package-lock.json
57 changes: 57 additions & 0 deletions lib/api-client.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Interfaces } from '@oclif/core';
import { CLIError } from '@oclif/core/lib/errors';
import { HTTP, HTTPError, HTTPRequestOptions } from 'http-call';
import { Login } from './login';
import { Mutex } from './mutex';
export declare namespace APIClient {
interface Options extends HTTPRequestOptions {
retryAuth?: boolean;
}
}
export interface IOptions {
required?: boolean;
preauth?: boolean;
}
export interface IHerokuAPIErrorOptions {
resource?: string;
app?: {
id: string;
name: string;
};
id?: string;
message?: string;
url?: string;
}
export declare class HerokuAPIError extends CLIError {
http: HTTPError;
body: IHerokuAPIErrorOptions;
constructor(httpError: HTTPError);
}
export declare class APIClient {
protected config: Interfaces.Config;
options: IOptions;
preauthPromises: {
[k: string]: Promise<HTTP<any>>;
};
authPromise?: Promise<HTTP<any>>;
http: typeof HTTP;
private readonly _login;
private _twoFactorMutex;
private _auth?;
constructor(config: Interfaces.Config, options?: IOptions);
get twoFactorMutex(): Mutex<string>;
get auth(): string | undefined;
set auth(token: string | undefined);
twoFactorPrompt(): Promise<string>;
preauth(app: string, factor: string): Promise<HTTP<unknown>>;
get<T>(url: string, options?: APIClient.Options): Promise<HTTP<T>>;
post<T>(url: string, options?: APIClient.Options): Promise<HTTP<T>>;
put<T>(url: string, options?: APIClient.Options): Promise<HTTP<T>>;
patch<T>(url: string, options?: APIClient.Options): Promise<HTTP<T>>;
delete<T>(url: string, options?: APIClient.Options): Promise<HTTP<T>>;
stream(url: string, options?: APIClient.Options): Promise<HTTP<unknown>>;
request<T>(url: string, options?: APIClient.Options): Promise<HTTP<T>>;
login(opts?: Login.Options): Promise<void>;
logout(): Promise<void>;
get defaults(): typeof HTTP.defaults;
}
194 changes: 194 additions & 0 deletions lib/api-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.APIClient = exports.HerokuAPIError = void 0;
const tslib_1 = require("tslib");
const errors_1 = require("@oclif/core/lib/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 request_id_1 = require("./request-id");
const vars_1 = require("./vars");
class HerokuAPIError extends errors_1.CLIError {
constructor(httpError) {
if (!httpError)
throw new Error('invalid error');
const options = httpError.body;
if (!options || !options.message)
throw httpError;
const 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 > 0)
super([options.message, '', ...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;
const apiUrl = url.URL ? new url.URL(vars_1.vars.apiUrl) : url.parse(vars_1.vars.apiUrl);
const envHeaders = JSON.parse(process.env.HEROKU_HEADERS || '{}');
this.preauthPromises = {};
const self = this;
const opts = {
host: apiUrl.hostname,
port: apiUrl.port,
protocol: apiUrl.protocol,
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);
}
// 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 trackRequestIds(response) {
const responseRequestIdHeader = response.headers[request_id_1.requestIdHeader];
if (responseRequestIdHeader) {
const requestIds = Array.isArray(responseRequestIdHeader) ? responseRequestIdHeader : responseRequestIdHeader.split(',');
request_id_1.RequestId.track(...requestIds);
}
}
static async request(url, opts = {}, retries = 3) {
opts.headers = opts.headers || {};
opts.headers[request_id_1.requestIdHeader] = request_id_1.RequestId.create() && request_id_1.RequestId.headerValue;
if (!Object.keys(opts.headers).find(h => h.toLowerCase() === 'authorization')) {
opts.headers.authorization = `Bearer ${self.auth}`;
}
retries--;
try {
const response = await super.request(url, opts);
this.trackRequestIds(response);
return response;
}
catch (error) {
if (!(error instanceof deps_1.default.HTTP.HTTPError))
throw error;
if (retries > 0) {
if (opts.retryAuth !== false && error.http.statusCode === 401 && error.body.id === 'unauthorized') {
if (process.env.HEROKU_API_KEY) {
throw new Error('The token provided to HEROKU_API_KEY is invalid. Please double-check that you have the correct token, or run `heroku login` without HEROKU_API_KEY set.');
}
if (!self.authPromise)
self.authPromise = self.login();
await self.authPromise;
opts.headers.authorization = `Bearer ${self.auth}`;
return this.request(url, opts, retries);
}
if (error.http.statusCode === 403 && error.body.id === 'two_factor') {
return this.twoFactorRetry(error, url, opts, retries);
}
}
throw new HerokuAPIError(error);
}
}
};
}
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 {
const factor = await deps_1.default.cli.prompt('Two-factor code', { type: 'mask' });
deps_1.default.yubikey.disable();
return factor;
}
catch (error) {
deps_1.default.yubikey.disable();
throw error;
}
});
}
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 (error) {
if (error instanceof errors_1.CLIError)
(0, errors_1.warn)(error);
}
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;
11 changes: 11 additions & 0 deletions lib/command.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Command as Base } from '@oclif/core';
import { APIClient } from './api-client';
export declare abstract class Command extends Base {
base: string;
_heroku: APIClient;
_legacyHerokuClient: any;
get heroku(): APIClient;
get legacyHerokuClient(): any;
get cli(): any;
get out(): any;
}
44 changes: 44 additions & 0 deletions lib/command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Command = void 0;
const tslib_1 = require("tslib");
const core_1 = require("@oclif/core");
const util_1 = require("util");
const pjson = require('../package.json');
const deps_1 = tslib_1.__importDefault(require("./deps"));
const deprecatedCLI = (0, util_1.deprecate)(() => {
return require('cli-ux').cli;
}, 'this.out and this.cli is deprecated. Please import "CliUx" from the @oclif/core module directly instead.');
class Command extends core_1.Command {
constructor() {
super(...arguments);
this.base = `${pjson.name}@${pjson.version}`;
}
get heroku() {
if (this._heroku)
return this._heroku;
this._heroku = new deps_1.default.APIClient(this.config);
return this._heroku;
}
get legacyHerokuClient() {
if (this._legacyHerokuClient)
return this._legacyHerokuClient;
const HerokuClient = require('heroku-client');
const options = {
debug: this.config.debug,
host: `${this.heroku.defaults.protocol || 'https:'}//${this.heroku.defaults.host ||
'api.heroku.com'}`,
token: this.heroku.auth,
userAgent: this.heroku.defaults.headers['user-agent'],
};
this._legacyHerokuClient = new HerokuClient(options);
return this._legacyHerokuClient;
}
get cli() {
return deprecatedCLI();
}
get out() {
return deprecatedCLI();
}
}
exports.Command = Command;
38 changes: 38 additions & 0 deletions lib/completions.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Interfaces } from '@oclif/core';
declare type CompletionContext = {
args?: {
[name: string]: string;
};
flags?: {
[name: string]: string;
};
argv?: string[];
config: Interfaces.Config;
};
declare type Completion = {
skipCache?: boolean;
cacheDuration?: number;
cacheKey?(ctx: CompletionContext): Promise<string>;
options(ctx: CompletionContext): Promise<string[]>;
};
export declare const oneDay: number;
export declare const herokuGet: (resource: string, ctx: {
config: Interfaces.Config;
}) => Promise<string[]>;
export declare const AppCompletion: Completion;
export declare const AppAddonCompletion: Completion;
export declare const AppDynoCompletion: Completion;
export declare const BuildpackCompletion: Completion;
export declare const DynoSizeCompletion: Completion;
export declare const FileCompletion: Completion;
export declare const PipelineCompletion: Completion;
export declare const ProcessTypeCompletion: Completion;
export declare const RegionCompletion: Completion;
export declare const RemoteCompletion: Completion;
export declare const RoleCompletion: Completion;
export declare const ScopeCompletion: Completion;
export declare const SpaceCompletion: Completion;
export declare const StackCompletion: Completion;
export declare const StageCompletion: Completion;
export declare const TeamCompletion: Completion;
export {};
Loading

0 comments on commit d37e880

Please sign in to comment.