diff --git a/packages/ionic/assets/sso/success/index.html b/packages/ionic/assets/sso/success/index.html new file mode 100644 index 0000000000..7737fb406d --- /dev/null +++ b/packages/ionic/assets/sso/success/index.html @@ -0,0 +1,51 @@ + + + + + Success + + + +
+

You are authenticated.

+

Please return to your terminal. You may close this window.

+
+ + diff --git a/packages/ionic/src/commands/git/remote.ts b/packages/ionic/src/commands/git/remote.ts index 0d1a8bff62..a55bb11825 100644 --- a/packages/ionic/src/commands/git/remote.ts +++ b/packages/ionic/src/commands/git/remote.ts @@ -30,7 +30,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://dashboard.ionicframework.com')} const token = this.env.session.getUserToken(); const proId = await this.project.requireProId(); - const appClient = new AppClient({ token, client: this.env.client }); + const appClient = new AppClient(token, this.env); const app = await appClient.load(proId); if (!app.repo_url) { diff --git a/packages/ionic/src/commands/link.ts b/packages/ionic/src/commands/link.ts index 306eb99594..1eb01cafcc 100644 --- a/packages/ionic/src/commands/link.ts +++ b/packages/ionic/src/commands/link.ts @@ -220,13 +220,13 @@ ${chalk.cyan('[2]')}: ${chalk.bold('https://ionicframework.com/support/request') private async getAppClient() { const { AppClient } = await import('../lib/app'); const token = this.env.session.getUserToken(); - return new AppClient({ token, client: this.env.client }); + return new AppClient(token, this.env); } private async getUserClient() { const { UserClient } = await import('../lib/user'); const token = this.env.session.getUserToken(); - return new UserClient({ token, client: this.env.client }); + return new UserClient(token, this.env); } async lookUpApp(proId: string): Promise { @@ -243,7 +243,8 @@ ${chalk.cyan('[2]')}: ${chalk.bold('https://ionicframework.com/support/request') async createApp({ name }: { name: string; }, runinfo: CommandInstanceInfo): Promise { const appClient = await this.getAppClient(); - const app = await appClient.create({ name }); + const org_id = this.env.config.get('org.id'); + const app = await appClient.create({ name, org_id }); await this.linkApp(app, runinfo); diff --git a/packages/ionic/src/commands/login.ts b/packages/ionic/src/commands/login.ts index a0e5525eaa..31a6d4d9d7 100644 --- a/packages/ionic/src/commands/login.ts +++ b/packages/ionic/src/commands/login.ts @@ -1,10 +1,9 @@ -import { validators } from '@ionic/cli-framework'; +import { OptionGroup, validators } from '@ionic/cli-framework'; import chalk from 'chalk'; -import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../definitions'; +import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../definitions'; import { Command } from '../lib/command'; import { FatalException } from '../lib/errors'; -import { runCommand } from '../lib/executor'; import { generateUUID } from '../lib/utils/uuid'; export class LoginCommand extends Command implements CommandPreRun { @@ -37,14 +36,25 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request') { name: 'password', summary: 'Your password', - validators: [validators.required], + // this is a hack since sso is hidden, no need to make password not required for it + validators: process.argv.includes('--sso') ? [] : [validators.required], private: true, }, ], + options: [ + { + name: 'sso', + type: Boolean, + summary: 'Open a window to log in with the SSO provider associated with your email', + groups: [OptionGroup.Hidden], + }, + ], }; } async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise { + const sso = !!options['sso']; + if (options['email'] || options['password']) { throw new FatalException( `${chalk.green('email')} and ${chalk.green('password')} are command arguments, not options. Please try this:\n` + @@ -52,20 +62,32 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request') ); } + const askForEmail = !inputs[0]; + const askForPassword = !sso && !inputs[1]; + if (this.env.session.isLoggedIn()) { - const extra = !inputs[0] || !inputs[1] ? 'Prompting for new credentials.' : 'Attempting login.'; const email = this.env.config.get('user.email'); - this.env.log.warn(`You are already logged in${email ? ' as ' + chalk.bold(email) : ''}! ${this.env.flags.interactive ? extra : ''}`); + + const extra = askForEmail || askForPassword + ? (this.env.flags.interactive ? `Prompting for new credentials.\n\nUse ${chalk.yellow('Ctrl+C')} to cancel and remain logged in.` : '') + : 'You will be logged out beforehand.'; + + this.env.log.warn( + 'You will be logged out.\n' + + `You are already logged in${email ? ' as ' + chalk.bold(email) : ''}! ${extra}` + ); + this.env.log.nl(); } else { - this.env.log.msg( - `Log into your Ionic Pro account\n` + - `If you don't have one yet, create yours by running: ${chalk.green(`ionic signup`)}\n` + this.env.log.info( + `Log into your Ionic Pro account!\n` + + `If you don't have one yet, create yours by running: ${chalk.green(`ionic signup`)}` ); + this.env.log.nl(); } // TODO: combine with promptToLogin ? - if (!inputs[0]) { + if (askForEmail) { const email = await this.env.prompt({ type: 'input', name: 'email', @@ -76,7 +98,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request') inputs[0] = email; } - if (!inputs[1]) { + if (askForPassword) { const password = await this.env.prompt({ type: 'password', name: 'password', @@ -89,17 +111,27 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request') } } - async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise { + async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise { const [ email, password ] = inputs; + const sso = !!options['sso']; if (this.env.session.isLoggedIn()) { - this.env.log.msg('Logging you out.'); - await runCommand(runinfo, ['logout']); + await this.env.session.logout(); this.env.config.set('tokens.telemetry', generateUUID()); } - await this.env.session.login(email, password); + if (sso) { + this.env.log.info( + `Ionic Pro SSO Login\n` + + `During this process, a browser window will open to authenticate you with the identity provider for ${chalk.green(email)}. Please leave this process running until authentication is complete.` + ); + this.env.log.nl(); + + await this.env.session.ssoLogin(email); + } else { + await this.env.session.login(email, password); + } - this.env.log.ok('You are logged in!'); + this.env.log.ok(chalk.green.bold('You are logged in!')); } } diff --git a/packages/ionic/src/commands/start.ts b/packages/ionic/src/commands/start.ts index 4bf46d14cc..14665a2b52 100644 --- a/packages/ionic/src/commands/start.ts +++ b/packages/ionic/src/commands/start.ts @@ -270,7 +270,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters if (proId) { const { AppClient } = await import('../lib/app'); const token = this.env.session.getUserToken(); - const appClient = new AppClient({ token, client: this.env.client }); + const appClient = new AppClient(token, this.env); const tasks = this.createTaskChain(); tasks.next(`Looking up app ${chalk.green(proId)}`); const app = await appClient.load(proId); diff --git a/packages/ionic/src/definitions.ts b/packages/ionic/src/definitions.ts index fa280ae42e..5544ae6042 100644 --- a/packages/ionic/src/definitions.ts +++ b/packages/ionic/src/definitions.ts @@ -326,6 +326,7 @@ export type NamespaceLocateResult = ζframework.NamespaceLocateResult; + ssoLogin(email: string): Promise; tokenLogin(token: string): Promise; logout(): Promise; @@ -385,6 +386,7 @@ export interface ConfigFile { 'git.host'?: string; 'git.port'?: number; 'git.setup'?: boolean; + 'org.id'?: string; 'user.id'?: number; 'user.email'?: string; 'tokens.user'?: string; diff --git a/packages/ionic/src/guards.ts b/packages/ionic/src/guards.ts index 570e01e6b6..451f08171b 100644 --- a/packages/ionic/src/guards.ts +++ b/packages/ionic/src/guards.ts @@ -28,6 +28,8 @@ import { User, } from './definitions'; +import { AuthConnection } from './lib/auth'; + export const INTEGRATION_NAMES: IntegrationName[] = ['capacitor', 'cordova']; export function isCommand(cmd: any): cmd is ICommand { @@ -219,6 +221,14 @@ export function isLoginResponse(res: APIResponse): res is Response { return isAPIResponseSuccess(res) && isLogin(res.data); } +export function isAuthConnection(connection: any): connection is AuthConnection { + return connection && typeof connection.uuid === 'string'; +} + +export function isAuthConnectionResponse(res: APIResponse): res is Response { + return isAPIResponseSuccess(res) && isAuthConnection(res.data); +} + export function isUser(user: any): user is User { return user && typeof user.id === 'number' diff --git a/packages/ionic/src/lib/app.ts b/packages/ionic/src/lib/app.ts index 6d612d8113..e1dc06f99c 100644 --- a/packages/ionic/src/lib/app.ts +++ b/packages/ionic/src/lib/app.ts @@ -15,27 +15,22 @@ export function formatName(app: Pick) { export interface AppClientDeps { readonly client: IClient; - readonly token: string; } export interface AppCreateDetails { - name: string; + readonly name: string; + readonly org_id?: string; } export class AppClient extends ResourceClient implements ResourceClientLoad, ResourceClientCreate, ResourceClientPaginate { - protected client: IClient; - protected token: string; - - constructor({ client, token }: AppClientDeps) { + constructor(readonly token: string, readonly e: AppClientDeps) { super(); - this.client = client; - this.token = token; } async load(id: string): Promise { - const { req } = await this.client.make('GET', `/apps/${id}`); + const { req } = await this.e.client.make('GET', `/apps/${id}`); this.applyAuthentication(req, this.token); - const res = await this.client.do(req); + const res = await this.e.client.do(req); if (!isAppResponse(res)) { throw createFatalAPIFormat(req, res); @@ -44,11 +39,11 @@ export class AppClient extends ResourceClient implements ResourceClientLoad return res.data; } - async create({ name }: AppCreateDetails): Promise { - const { req } = await this.client.make('POST', '/apps'); + async create(details: AppCreateDetails): Promise { + const { req } = await this.e.client.make('POST', '/apps'); this.applyAuthentication(req, this.token); - req.send({ name }); - const res = await this.client.do(req); + req.send(details); + const res = await this.e.client.do(req); if (!isAppResponse(res)) { throw createFatalAPIFormat(req, res); @@ -58,9 +53,9 @@ export class AppClient extends ResourceClient implements ResourceClientLoad } paginate(args: Partial>> = {}): IPaginator, PaginatorState> { - return this.client.paginate({ + return this.e.client.paginate({ reqgen: async () => { - const { req } = await this.client.make('GET', '/apps'); + const { req } = await this.e.client.make('GET', '/apps'); this.applyAuthentication(req, this.token); return { req }; }, @@ -70,17 +65,17 @@ export class AppClient extends ResourceClient implements ResourceClientLoad } async createAssociation(id: string, association: { repoId: number; type: AssociationType; branches: string[] }): Promise { - const { req } = await this.client.make('POST', `/apps/${id}/repository`); + const { req } = await this.e.client.make('POST', `/apps/${id}/repository`); req .set('Authorization', `Bearer ${this.token}`) - .send({ - repository_id: association.repoId, - type: association.type, - branches: association.branches, - }); + .send({ + repository_id: association.repoId, + type: association.type, + branches: association.branches, + }); - const res = await this.client.do(req); + const res = await this.e.client.do(req); if (!isAppAssociationResponse(res)) { throw createFatalAPIFormat(req, res); @@ -90,7 +85,7 @@ export class AppClient extends ResourceClient implements ResourceClientLoad } async deleteAssociation(id: string): Promise { - const { req } = await this.client.make('DELETE', `/apps/${id}/repository`); + const { req } = await this.e.client.make('DELETE', `/apps/${id}/repository`); req .set('Authorization', `Bearer ${this.token}`) diff --git a/packages/ionic/src/lib/auth.ts b/packages/ionic/src/lib/auth.ts new file mode 100644 index 0000000000..f437b657ae --- /dev/null +++ b/packages/ionic/src/lib/auth.ts @@ -0,0 +1,38 @@ +import { IClient, ResourceClientLoad } from '../definitions'; +import { isAuthConnectionResponse } from '../guards'; + +import { ResourceClient, createFatalAPIFormat } from './http'; + +export interface AuthConnection { + readonly uuid: string; +} + +export interface AuthClientDeps { + readonly client: IClient; +} + +export class AuthClient extends ResourceClient { + readonly connections: AuthConnectionClient; + + constructor(readonly e: AuthClientDeps) { + super(); + this.connections = new AuthConnectionClient(e); + } +} + +export class AuthConnectionClient extends ResourceClient implements ResourceClientLoad { + constructor(readonly e: AuthClientDeps) { + super(); + } + + async load(email: string): Promise { + const { req } = await this.e.client.make('GET', `/auth/connections/${email}`); + const res = await this.e.client.do(req); + + if (!isAuthConnectionResponse(res)) { + throw createFatalAPIFormat(req, res); + } + + return res.data; + } +} diff --git a/packages/ionic/src/lib/doctor/ailments/index.ts b/packages/ionic/src/lib/doctor/ailments/index.ts index adf0430d0d..45e968a411 100644 --- a/packages/ionic/src/lib/doctor/ailments/index.ts +++ b/packages/ionic/src/lib/doctor/ailments/index.ts @@ -159,7 +159,7 @@ export class GitConfigInvalid extends Ailment { } const token = this.session.getUserToken(); - const appClient = new AppClient({ token, client: this.client }); + const appClient = new AppClient(token, { client: this.client }); const app = await appClient.load(proId); if (app.repo_url !== remote) { diff --git a/packages/ionic/src/lib/session.ts b/packages/ionic/src/lib/session.ts index 1302888d57..64688cf02f 100644 --- a/packages/ionic/src/lib/session.ts +++ b/packages/ionic/src/lib/session.ts @@ -12,27 +12,22 @@ export interface SessionDeps { } export class BaseSession { - protected config: IConfig; - protected client: IClient; - - constructor({ config, client }: SessionDeps) { - this.config = config; - this.client = client; - } + constructor(readonly e: SessionDeps) {} async logout(): Promise { - this.config.unset('user.id'); - this.config.unset('user.email'); - this.config.unset('tokens.user'); - this.config.set('git.setup', false); + this.e.config.unset('org.id'); + this.e.config.unset('user.id'); + this.e.config.unset('user.email'); + this.e.config.unset('tokens.user'); + this.e.config.set('git.setup', false); } isLoggedIn(): boolean { - return typeof this.config.get('tokens.user') === 'string'; + return typeof this.e.config.get('tokens.user') === 'string'; } getUser(): { id: number; } { - const userId = this.config.get('user.id'); + const userId = this.e.config.get('user.id'); if (!userId) { throw new SessionException( @@ -45,7 +40,7 @@ export class BaseSession { } getUserToken(): string { - const userToken = this.config.get('tokens.user'); + const userToken = this.e.config.get('tokens.user'); if (!userToken) { throw new SessionException( @@ -60,11 +55,11 @@ export class BaseSession { export class ProSession extends BaseSession implements ISession { async login(email: string, password: string): Promise { - const { req } = await this.client.make('POST', '/login'); + const { req } = await this.e.client.make('POST', '/login'); req.send({ email, password, source: 'cli' }); try { - const res = await this.client.do(req); + const res = await this.e.client.do(req); if (!isLoginResponse(res)) { const data = res.data; @@ -81,13 +76,13 @@ export class ProSession extends BaseSession implements ISession { const { token, user } = res.data; - if (this.config.get('user.id') !== user.id) { // User changed + if (this.e.config.get('user.id') !== user.id) { // User changed await this.logout(); } - this.config.set('user.id', user.id); - this.config.set('user.email', email); - this.config.set('tokens.user', token); + this.e.config.set('user.id', user.id); + this.e.config.set('user.email', email); + this.e.config.set('tokens.user', token); } catch (e) { if (isSuperAgentError(e) && (e.response.status === 401 || e.response.status === 403)) { throw new SessionException('Incorrect email or password.'); @@ -97,22 +92,37 @@ export class ProSession extends BaseSession implements ISession { } } - async tokenLogin(token: string) { + async ssoLogin(email: string): Promise { + const { AuthClient } = await import('./auth'); + const { Auth0OAuth2Flow } = await import('./sso'); + + const authClient = new AuthClient(this.e); + const { uuid: connection } = await authClient.connections.load(email); + + const flow = new Auth0OAuth2Flow({ email, connection }, this.e); + const token = await flow.run(); + + await this.tokenLogin(token); + + this.e.config.set('org.id', connection); + } + + async tokenLogin(token: string): Promise { const { UserClient } = await import('./user'); - const userClient = new UserClient({ client: this.client, token }); + const userClient = new UserClient(token, this.e); try { const user = await userClient.loadSelf(); const user_id = user.id; - if (this.config.get('user.id') !== user_id) { // User changed + if (this.e.config.get('user.id') !== user_id) { // User changed await this.logout(); } - this.config.set('user.id', user_id); - this.config.set('user.email', user.email); - this.config.set('tokens.user', token); + this.e.config.set('user.id', user_id); + this.e.config.set('user.email', user.email); + this.e.config.set('tokens.user', token); } catch (e) { if (isSuperAgentError(e) && (e.response.status === 401 || e.response.status === 403)) { throw new SessionException('Invalid auth token.'); diff --git a/packages/ionic/src/lib/sso.ts b/packages/ionic/src/lib/sso.ts new file mode 100644 index 0000000000..c0cf84f547 --- /dev/null +++ b/packages/ionic/src/lib/sso.ts @@ -0,0 +1,179 @@ +import { readFile } from '@ionic/utils-fs'; +import { isPortAvailable } from '@ionic/utils-network'; +import * as crypto from 'crypto'; +import * as http from 'http'; +import * as path from 'path'; +import * as qs from 'querystring'; + +import { ASSETS_DIRECTORY } from '../constants'; +import { IClient } from '../definitions'; + +const REDIRECT_PORT = 8123; +const REDIRECT_HOST = 'localhost'; + +export interface AuthorizationParameters { + [key: string]: string; +} + +export interface TokenParameters { + [key: string]: string; +} + +export interface OAuth2FlowOptions { + readonly authorizationUrl: string; + readonly tokenUrl: string; + readonly clientId: string; + readonly redirectHost?: string; + readonly redirectPort?: number; +} + +export interface OAuth2FlowDeps { + readonly client: IClient; +} + +export abstract class OAuth2Flow { + readonly authorizationUrl: string; + readonly tokenUrl: string; + readonly clientId: string; + readonly redirectHost: string; + readonly redirectPort: number; + + constructor({ authorizationUrl, tokenUrl, clientId, redirectHost = REDIRECT_HOST, redirectPort = REDIRECT_PORT }: OAuth2FlowOptions, readonly e: OAuth2FlowDeps) { + this.authorizationUrl = authorizationUrl; + this.tokenUrl = tokenUrl; + this.clientId = clientId; + this.redirectHost = redirectHost; + this.redirectPort = redirectPort; + } + + get redirectUrl(): string { + return `http://${this.redirectHost}:${this.redirectPort}`; + } + + async run(): Promise { + const opn = await import('opn'); + + const verifier = this.generateVerifier(); + const challenge = this.generateChallenge(verifier); + + const authorizationParams = this.generateAuthorizationParameters(challenge); + const authorizationUrl = `${this.authorizationUrl}?${qs.stringify(authorizationParams)}`; + + await opn(authorizationUrl, { wait: false }); + + const authorizationCode = await this.getAuthorizationCode(); + const token = await this.getAccessToken(authorizationCode, verifier); + + return token; + } + + protected abstract generateAuthorizationParameters(challenge: string): AuthorizationParameters; + protected abstract generateTokenParameters(authorizationCode: string, verifier: string): TokenParameters; + + protected async getSuccessHtml(): Promise { + const p = path.resolve(ASSETS_DIRECTORY, 'sso', 'success', 'index.html'); + const contents = await readFile(p, { encoding: 'utf8' }); + + return contents; + } + + protected async getAuthorizationCode(): Promise { + if (!(await isPortAvailable(this.redirectPort))) { + throw new Error(`Cannot start local server. Port ${this.redirectPort} is in use.`); + } + + const successHtml = await this.getSuccessHtml(); + + return new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + if (req.url) { + const params = qs.parse(req.url.substring(req.url.indexOf('?') + 1)); + + if (params.code) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(successHtml); + req.socket.destroy(); + server.close(); + + resolve(params.code); + } + + // TODO, timeout, error handling + } + }); + + server.listen(this.redirectPort, this.redirectHost); + }); + } + + protected async getAccessToken(authorizationCode: string, verifier: string): Promise { + const params = this.generateTokenParameters(authorizationCode, verifier); + const { req } = await this.e.client.make('POST', this.tokenUrl); + + const res = await req.send(params); + + return res.body.access_token; + } + + protected generateVerifier(): string { + return this.base64URLEncode(crypto.randomBytes(32)); + } + + protected generateChallenge(verifier: string): string { + return this.base64URLEncode(crypto.createHash('sha256').update(verifier).digest()); + } + + protected base64URLEncode(buffer: Buffer) { + return buffer.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + } +} + +const AUTHORIZATION_URL = 'https://auth.ionicframework.com/authorize'; +const TOKEN_URL = 'https://auth.ionicframework.com/oauth/token'; +const CLIENT_ID = '0kTF4wm74vppjImr11peCjQo2PIQDS3m'; +const API_AUDIENCE = 'https://api.ionicjs.com'; + +export interface Auth0OAuth2FlowOptions extends Partial { + readonly email: string; + readonly connection: string; + readonly audience?: string; +} + +export class Auth0OAuth2Flow extends OAuth2Flow { + readonly email: string; + readonly audience: string; + readonly connection: string; + + constructor({ email, connection, audience = API_AUDIENCE, authorizationUrl = AUTHORIZATION_URL, tokenUrl = TOKEN_URL, clientId = CLIENT_ID, ...options }: Auth0OAuth2FlowOptions, readonly e: OAuth2FlowDeps) { + super({ authorizationUrl, tokenUrl, clientId, ...options }, e); + this.email = email; + this.connection = connection; + this.audience = audience; + } + + protected generateAuthorizationParameters(challenge: string): AuthorizationParameters { + return { + audience: this.audience, + scope: 'openid profile email offline_access', + response_type: 'code', + connection: this.connection, + client_id: this.clientId, + code_challenge: challenge, + code_challenge_method: 'S256', + redirect_uri: this.redirectUrl, + }; + } + + protected generateTokenParameters(code: string, verifier: string): TokenParameters { + return { + grant_type: 'authorization_code', + client_id: this.clientId, + code_verifier: verifier, + code, + redirect_uri: this.redirectUrl, + }; + } +} diff --git a/packages/ionic/src/lib/user.ts b/packages/ionic/src/lib/user.ts index 24733ac8b3..861667b5c1 100644 --- a/packages/ionic/src/lib/user.ts +++ b/packages/ionic/src/lib/user.ts @@ -5,24 +5,18 @@ import { ResourceClient, TokenPaginator, createFatalAPIFormat } from './http'; export interface UserClientDeps { readonly client: IClient; - readonly token: string; } export class UserClient extends ResourceClient implements ResourceClientLoad { - protected client: IClient; - protected token: string; - - constructor({ client, token }: UserClientDeps) { + constructor(readonly token: string, readonly e: UserClientDeps) { super(); - this.client = client; - this.token = token; } async load(id: number, modifiers?: ResourceClientRequestModifiers): Promise { - const { req } = await this.client.make('GET', `/users/${id}`); + const { req } = await this.e.client.make('GET', `/users/${id}`); this.applyAuthentication(req, this.token); this.applyModifiers(req, modifiers); - const res = await this.client.do(req); + const res = await this.e.client.do(req); if (!isUserResponse(res)) { throw createFatalAPIFormat(req, res); @@ -32,9 +26,9 @@ export class UserClient extends ResourceClient implements ResourceClientLoad { - const { req } = await this.client.make('GET', '/users/self'); + const { req } = await this.e.client.make('GET', '/users/self'); this.applyAuthentication(req, this.token); - const res = await this.client.do(req); + const res = await this.e.client.do(req); if (!isUserResponse(res)) { throw createFatalAPIFormat(req, res); @@ -44,11 +38,11 @@ export class UserClient extends ResourceClient implements ResourceClientLoad { - const { req } = await this.client.make('POST', `/users/${id}/oauth/github`); + const { req } = await this.e.client.make('POST', `/users/${id}/oauth/github`); this.applyAuthentication(req, this.token); req.send({ source: 'cli' }); - const res = await this.client.do(req); + const res = await this.e.client.do(req); if (!isOAuthLoginResponse(res)) { throw createFatalAPIFormat(req, res); @@ -59,9 +53,9 @@ export class UserClient extends ResourceClient implements ResourceClientLoad, TokenPaginatorState> { return new TokenPaginator({ - client: this.client, + client: this.e.client, reqgen: async () => { - const { req } = await this.client.make('GET', `/users/${id}/oauth/github/repositories`); + const { req } = await this.e.client.make('GET', `/users/${id}/oauth/github/repositories`); req.set('Authorization', `Bearer ${this.token}`); return { req }; }, @@ -71,9 +65,9 @@ export class UserClient extends ResourceClient implements ResourceClientLoad, TokenPaginatorState> { return new TokenPaginator({ - client: this.client, + client: this.e.client, reqgen: async () => { - const { req } = await this.client.make('GET', `/users/${userId}/oauth/github/repositories/${repoId}/branches`); + const { req } = await this.e.client.make('GET', `/users/${userId}/oauth/github/repositories/${repoId}/branches`); req.set('Authorization', `Bearer ${this.token}`); return { req }; },