UNPKG

@redocly/openapi-core

Version:

See https://github.com/Redocly/openapi-cli

210 lines (175 loc) 6.33 kB
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'; import { resolve } from 'path'; import { homedir } from 'os'; import { red, green, gray, yellow } from 'colorette'; import { RegistryApi } from './registry-api'; import { AccessTokens, DEFAULT_REGION, DOMAINS, Region, AVAILABLE_REGIONS } from '../config/config'; import { RegionalToken, RegionalTokenWithValidity } from './redocly-client-types'; import { isNotEmptyObject } from '../utils'; const TOKEN_FILENAME = '.redocly-config.json'; let REDOCLY_DOMAIN: string; // workaround for the isRedoclyRegistryURL, see more below export class RedoclyClient { private accessTokens: AccessTokens = {}; private region: Region; domain: string; registryApi: RegistryApi; constructor(region?: Region) { this.region = this.loadRegion(region); this.loadTokens(); this.domain = region ? DOMAINS[region] : process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION]; /* * We can't use process.env here because it is replaced by a const in some client-side bundles, * which breaks assignment. */ REDOCLY_DOMAIN = this.domain; // isRedoclyRegistryURL depends on the value to be set this.registryApi = new RegistryApi(this.accessTokens, this.region); } loadRegion(region?: Region) { if (region && !DOMAINS[region]) { process.stdout.write( red( `Invalid argument: region in config file.\nGiven: ${green( region, )}, choices: "us", "eu".\n`, ), ); process.exit(1); } if (process.env.REDOCLY_DOMAIN) { return (AVAILABLE_REGIONS.find( (region) => DOMAINS[region as Region] === process.env.REDOCLY_DOMAIN, ) || DEFAULT_REGION) as Region; } return region || DEFAULT_REGION; } getRegion(): Region { return this.region; } hasTokens(): boolean { return isNotEmptyObject(this.accessTokens); } // <backward compatibility: old versions of portal> hasToken() { return !!this.accessTokens[this.region]; } async getAuthorizationHeader(): Promise<string | undefined> { const token = this.accessTokens[this.region]; // print this only if there is token but invalid if (token && !this.isAuthorizedWithRedoclyByRegion()) { process.stderr.write( `${yellow( 'Warning:', )} invalid Redocly API key. Use "npx @redocly/openapi-cli login" to provide your API key\n`, ); return undefined; } return token; } // </backward compatibility: portal> setAccessTokens(accessTokens: AccessTokens) { this.accessTokens = accessTokens; } loadTokens(): void { const credentialsPath = resolve(homedir(), TOKEN_FILENAME); const credentials = this.readCredentialsFile(credentialsPath); if (isNotEmptyObject(credentials)) { this.setAccessTokens({ ...credentials, ...(credentials.token && !credentials[this.region] && { [this.region]: credentials.token, }), }); } if (process.env.REDOCLY_AUTHORIZATION) { this.setAccessTokens({ ...this.accessTokens, [this.region]: process.env.REDOCLY_AUTHORIZATION, }); } } getAllTokens(): RegionalToken[] { return (<[Region, string][]>Object.entries(this.accessTokens)) .filter(([region]) => AVAILABLE_REGIONS.includes(region)) .map(([region, token]) => ({ region, token })); } async getValidTokens(): Promise<RegionalTokenWithValidity[]> { const allTokens = this.getAllTokens(); const verifiedTokens = await Promise.allSettled( allTokens.map(({ token, region }) => this.verifyToken(token, region)), ); return allTokens .filter((_, index) => verifiedTokens[index].status === 'fulfilled') .map(({ token, region }) => ({ token, region, valid: true })); } async getTokens() { return this.hasTokens() ? await this.getValidTokens() : []; } async isAuthorizedWithRedoclyByRegion(): Promise<boolean> { if (!this.hasTokens()) { return false; } const accessToken = this.accessTokens[this.region]; if (!accessToken) { return false; } try { await this.verifyToken(accessToken, this.region); return true; } catch (err) { return false; } } async isAuthorizedWithRedocly(): Promise<boolean> { return this.hasTokens() && isNotEmptyObject(await this.getValidTokens()); } readCredentialsFile(credentialsPath: string) { return existsSync(credentialsPath) ? JSON.parse(readFileSync(credentialsPath, 'utf-8')) : {}; } async verifyToken( accessToken: string, region: Region, verbose: boolean = false, ): Promise<{ viewerId: string; organizations: string[] }> { return this.registryApi.authStatus(accessToken, region, verbose); } async login(accessToken: string, verbose: boolean = false) { const credentialsPath = resolve(homedir(), TOKEN_FILENAME); process.stdout.write(gray('\n Logging in...\n')); try { await this.verifyToken(accessToken, this.region, verbose); } catch (err) { process.stdout.write( red('Authorization failed. Please check if you entered a valid API key.\n'), ); process.exit(1); } const credentials = { ...this.readCredentialsFile(credentialsPath), [this.region!]: accessToken, token: accessToken, // FIXME: backward compatibility, remove on 1.0.0 }; this.accessTokens = credentials; this.registryApi.setAccessTokens(credentials); writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2)); process.stdout.write(green(' Authorization confirmed. ✅\n\n')); } logout(): void { const credentialsPath = resolve(homedir(), TOKEN_FILENAME); if (existsSync(credentialsPath)) { unlinkSync(credentialsPath); } process.stdout.write('Logged out from the Redocly account. ✋\n'); } } export function isRedoclyRegistryURL(link: string): boolean { const domain = REDOCLY_DOMAIN || process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION]; const legacyDomain = domain === 'redocly.com' ? 'redoc.ly' : domain; if ( !link.startsWith(`https://api.${domain}/registry/`) && !link.startsWith(`https://api.${legacyDomain}/registry/`) ) { return false; } return true; }