@redocly/openapi-core
Version:
See https://github.com/Redocly/openapi-cli
210 lines (175 loc) • 6.33 kB
text/typescript
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;
}