@redocly/openapi-core
Version:
See https://github.com/Redocly/redocly-cli
178 lines (147 loc) • 5 kB
text/typescript
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
import { resolve } from 'path';
import { homedir } from 'os';
import { RegistryApi } from './registry-api';
import { env } from '../env';
import { isNotEmptyArray, isNotEmptyObject } from '../utils';
import { colorize } from '../logger';
import {
AVAILABLE_REGIONS,
DEFAULT_REGION,
DOMAINS,
getRedoclyDomain,
setRedoclyDomain,
} from './domains';
import type { RegionalToken, RegionalTokenWithValidity } from './redocly-client-types';
import type { AccessTokens, Region } from '../config/types';
export const TOKEN_FILENAME = '.redocly-config.json';
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] : env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
setRedoclyDomain(this.domain);
this.registryApi = new RegistryApi(this.accessTokens, this.region);
}
loadRegion(region?: Region) {
if (region && !DOMAINS[region]) {
throw new Error(
`Invalid argument: region in config file.\nGiven: ${colorize.green(
region
)}, choices: "us", "eu".`
);
}
if (getRedoclyDomain()) {
return (AVAILABLE_REGIONS.find(
(region) => DOMAINS[region as Region] === getRedoclyDomain()
) || 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> {
return this.accessTokens[this.region];
}
// </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 (env.REDOCLY_AUTHORIZATION) {
this.setAccessTokens({
...this.accessTokens,
[this.region]: 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 }) => this.verifyToken(token))
);
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);
return true;
} catch (err) {
return false;
}
}
async isAuthorizedWithRedocly(): Promise<boolean> {
return this.hasTokens() && isNotEmptyArray(await this.getValidTokens());
}
readCredentialsFile(credentialsPath: string) {
return existsSync(credentialsPath) ? JSON.parse(readFileSync(credentialsPath, 'utf-8')) : {};
}
async verifyToken(
accessToken: string,
verbose: boolean = false
): Promise<{ viewerId: string; organizations: string[] }> {
return this.registryApi.authStatus(accessToken, verbose);
}
async login(accessToken: string, verbose: boolean = false) {
const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
try {
await this.verifyToken(accessToken, verbose);
} catch (err) {
throw new Error('Authorization failed. Please check if you entered a valid API key.');
}
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));
}
logout(): void {
const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
if (existsSync(credentialsPath)) {
unlinkSync(credentialsPath);
}
}
}