@redocly/openapi-core 1.0.0-beta.69 → 1.0.0-beta.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/__tests__/lint.test.ts +1 -1
  2. package/__tests__/login.test.ts +17 -0
  3. package/lib/config/all.js +2 -0
  4. package/lib/config/config.d.ts +10 -0
  5. package/lib/config/config.js +12 -1
  6. package/lib/config/load.js +17 -8
  7. package/lib/index.d.ts +2 -2
  8. package/lib/redocly/index.d.ts +24 -5
  9. package/lib/redocly/index.js +82 -30
  10. package/lib/redocly/registry-api.d.ts +8 -5
  11. package/lib/redocly/registry-api.js +31 -20
  12. package/lib/rules/common/no-invalid-parameter-examples.d.ts +1 -0
  13. package/lib/rules/common/no-invalid-parameter-examples.js +25 -0
  14. package/lib/rules/common/no-invalid-schema-examples.d.ts +1 -0
  15. package/lib/rules/common/no-invalid-schema-examples.js +23 -0
  16. package/lib/rules/common/paths-kebab-case.js +1 -1
  17. package/lib/rules/oas2/index.d.ts +2 -0
  18. package/lib/rules/oas2/index.js +4 -0
  19. package/lib/rules/oas3/index.js +4 -0
  20. package/lib/rules/oas3/no-invalid-media-type-examples.js +5 -26
  21. package/lib/rules/utils.d.ts +3 -0
  22. package/lib/rules/utils.js +26 -1
  23. package/lib/typings/openapi.d.ts +3 -0
  24. package/lib/utils.d.ts +1 -0
  25. package/lib/utils.js +5 -1
  26. package/package.json +2 -2
  27. package/src/config/__tests__/load.test.ts +35 -0
  28. package/src/config/all.ts +2 -0
  29. package/src/config/config.ts +17 -0
  30. package/src/config/load.ts +20 -9
  31. package/src/index.ts +2 -8
  32. package/src/redocly/__tests__/redocly-client.test.ts +120 -0
  33. package/src/redocly/index.ts +102 -30
  34. package/src/redocly/registry-api.ts +33 -29
  35. package/src/rules/common/__tests__/paths-kebab-case.test.ts +23 -0
  36. package/src/rules/common/no-invalid-parameter-examples.ts +36 -0
  37. package/src/rules/common/no-invalid-schema-examples.ts +27 -0
  38. package/src/rules/common/paths-kebab-case.ts +1 -1
  39. package/src/rules/oas2/index.ts +4 -0
  40. package/src/rules/oas3/index.ts +4 -0
  41. package/src/rules/oas3/no-invalid-media-type-examples.ts +16 -36
  42. package/src/rules/utils.ts +43 -2
  43. package/src/typings/openapi.ts +4 -0
  44. package/src/utils.ts +5 -1
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSuggest = exports.validateDefinedAndNonEmpty = exports.fieldNonEmpty = exports.missingRequiredField = exports.matchesJsonSchemaType = exports.oasTypeOf = void 0;
3
+ exports.validateExample = exports.getSuggest = exports.validateDefinedAndNonEmpty = exports.fieldNonEmpty = exports.missingRequiredField = exports.matchesJsonSchemaType = exports.oasTypeOf = void 0;
4
4
  const levenshtein = require("js-levenshtein");
5
+ const ref_utils_1 = require("../ref-utils");
6
+ const ajv_1 = require("./ajv");
5
7
  function oasTypeOf(value) {
6
8
  if (Array.isArray(value)) {
7
9
  return 'array';
@@ -80,3 +82,26 @@ function getSuggest(given, variants) {
80
82
  return distances.map((d) => d.variant);
81
83
  }
82
84
  exports.getSuggest = getSuggest;
85
+ function validateExample(example, schema, dataLoc, { resolve, location, report }, disallowAdditionalProperties) {
86
+ try {
87
+ const { valid, errors } = ajv_1.validateJsonSchema(example, schema, location.child('schema'), dataLoc.pointer, resolve, disallowAdditionalProperties);
88
+ if (!valid) {
89
+ for (let error of errors) {
90
+ report({
91
+ message: `Example value must conform to the schema: ${error.message}.`,
92
+ location: Object.assign(Object.assign({}, new ref_utils_1.Location(dataLoc.source, error.instancePath)), { reportOnKey: error.keyword === 'additionalProperties' }),
93
+ from: location,
94
+ suggest: error.suggest,
95
+ });
96
+ }
97
+ }
98
+ }
99
+ catch (e) {
100
+ report({
101
+ message: `Example validation errored: ${e.message}.`,
102
+ location: location.child('schema'),
103
+ from: location,
104
+ });
105
+ }
106
+ }
107
+ exports.validateExample = validateExample;
@@ -144,6 +144,9 @@ export interface Oas3Schema {
144
144
  example?: any;
145
145
  xml?: Oas3Xml;
146
146
  }
147
+ export interface Oas3_1Schema extends Oas3Schema {
148
+ examples?: any[];
149
+ }
147
150
  export interface Oas3Discriminator {
148
151
  propertyName: string;
149
152
  mapping?: {
package/lib/utils.d.ts CHANGED
@@ -33,3 +33,4 @@ export declare function isPathParameter(pathSegment: string): boolean;
33
33
  * Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar
34
34
  */
35
35
  export declare function slash(path: string): string;
36
+ export declare function isNotEmptyObject(obj: any): boolean;
package/lib/utils.js CHANGED
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.slash = exports.isPathParameter = exports.readFileAsStringSync = exports.isSingular = exports.validateMimeTypeOAS3 = exports.validateMimeType = exports.splitCamelCaseIntoWords = exports.omitObjectProps = exports.pickObjectProps = exports.match = exports.readFileFromUrl = exports.isPlainObject = exports.notUndefined = exports.loadYaml = exports.popStack = exports.pushStack = exports.stringifyYaml = exports.parseYaml = void 0;
12
+ exports.isNotEmptyObject = exports.slash = exports.isPathParameter = exports.readFileAsStringSync = exports.isSingular = exports.validateMimeTypeOAS3 = exports.validateMimeType = exports.splitCamelCaseIntoWords = exports.omitObjectProps = exports.pickObjectProps = exports.match = exports.readFileFromUrl = exports.isPlainObject = exports.notUndefined = exports.loadYaml = exports.popStack = exports.pushStack = exports.stringifyYaml = exports.parseYaml = void 0;
13
13
  const fs = require("fs");
14
14
  const minimatch = require("minimatch");
15
15
  const node_fetch_1 = require("node-fetch");
@@ -144,3 +144,7 @@ function slash(path) {
144
144
  return path.replace(/\\/g, '/');
145
145
  }
146
146
  exports.slash = slash;
147
+ function isNotEmptyObject(obj) {
148
+ return !!obj && Object.keys(obj).length > 0;
149
+ }
150
+ exports.isNotEmptyObject = isNotEmptyObject;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/openapi-core",
3
- "version": "1.0.0-beta.69",
3
+ "version": "1.0.0-beta.73",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "engines": {
@@ -50,4 +50,4 @@
50
50
  "@types/pluralize": "^0.0.29",
51
51
  "typescript": "^4.0.5"
52
52
  }
53
- }
53
+ }
@@ -0,0 +1,35 @@
1
+ import { loadConfig } from '../load';
2
+ import { RedoclyClient } from '../../redocly';
3
+
4
+ describe('loadConfig', () => {
5
+ it('should resolve config http header by US region', async () => {
6
+ jest.spyOn(RedoclyClient.prototype, 'getTokens').mockImplementation(
7
+ () => Promise.resolve([{ region: 'us', token: "accessToken", valid: true }])
8
+ );
9
+ const config = await loadConfig();
10
+ expect(config.resolve.http.headers).toStrictEqual([{
11
+ "matches": 'https://api.redoc.ly/registry/**',
12
+ "name": "Authorization",
13
+ "envVariable": undefined,
14
+ "value": "accessToken"
15
+ }, {
16
+ "matches": 'https://api.redocly.com/registry/**',
17
+ "name": "Authorization",
18
+ "envVariable": undefined,
19
+ "value": "accessToken"
20
+ }]);
21
+ });
22
+
23
+ it('should resolve config http header by EU region', async () => {
24
+ jest.spyOn(RedoclyClient.prototype, 'getTokens').mockImplementation(
25
+ () => Promise.resolve([{ region: 'eu', token: "accessToken", valid: true }])
26
+ );
27
+ const config = await loadConfig();
28
+ expect(config.resolve.http.headers).toStrictEqual([{
29
+ "matches": 'https://api.eu.redocly.com/registry/**',
30
+ "name": "Authorization",
31
+ "envVariable": undefined,
32
+ "value": "accessToken"
33
+ }]);
34
+ });
35
+ });
package/src/config/all.ts CHANGED
@@ -38,6 +38,8 @@ export default {
38
38
  },
39
39
  'request-mime-type': 'error',
40
40
  spec: 'error',
41
+ 'no-invalid-schema-examples': 'error',
42
+ 'no-invalid-parameter-examples': 'error',
41
43
  },
42
44
  oas3_0Rules: {
43
45
  'no-invalid-media-type-examples': 'error',
@@ -123,11 +123,26 @@ export type ResolveConfig = {
123
123
  http: HttpResolveConfig;
124
124
  };
125
125
 
126
+ export const DEFAULT_REGION = 'us';
127
+ export type Region = 'us' | 'eu';
128
+ export type AccessTokens = {[region in Region]?: string };
129
+ const REDOCLY_DOMAIN = process.env.REDOCLY_DOMAIN;
130
+ export const DOMAINS: { [region in Region]: string } = {
131
+ us: 'redoc.ly',
132
+ eu: 'eu.redocly.com',
133
+ };
134
+
135
+ // FIXME: temporary fix for our lab environments
136
+ if (REDOCLY_DOMAIN?.endsWith('.redocly.host')) {
137
+ DOMAINS[REDOCLY_DOMAIN.split('.')[0] as Region] = REDOCLY_DOMAIN;
138
+ }
139
+
126
140
  export type RawConfig = {
127
141
  referenceDocs?: any;
128
142
  apiDefinitions?: Record<string, string>;
129
143
  lint?: LintRawConfig;
130
144
  resolve?: RawResolveConfig;
145
+ region?: Region;
131
146
  };
132
147
 
133
148
  export class LintConfig {
@@ -385,6 +400,7 @@ export class Config {
385
400
  lint: LintConfig;
386
401
  resolve: ResolveConfig;
387
402
  licenseKey?: string;
403
+ region?: Region;
388
404
  constructor(public rawConfig: RawConfig, public configFile?: string) {
389
405
  this.apiDefinitions = rawConfig.apiDefinitions || {};
390
406
  this.lint = new LintConfig(rawConfig.lint || {}, configFile);
@@ -395,6 +411,7 @@ export class Config {
395
411
  customFetch: undefined,
396
412
  },
397
413
  };
414
+ this.region = rawConfig.region;
398
415
  }
399
416
  }
400
417
 
@@ -1,8 +1,7 @@
1
1
  import * as fs from 'fs';
2
-
3
2
  import { RedoclyClient } from '../redocly';
4
3
  import { loadYaml } from '../utils';
5
- import { Config, RawConfig } from './config';
4
+ import { Config, DOMAINS, RawConfig, Region } from './config';
6
5
 
7
6
  import { defaultPlugin } from './builtIn';
8
7
 
@@ -25,19 +24,31 @@ export async function loadConfig(configPath?: string, customExtends?: string[]):
25
24
  }
26
25
 
27
26
  const redoclyClient = new RedoclyClient();
28
- if (redoclyClient.hasToken()) {
27
+ const tokens = await redoclyClient.getTokens();
28
+
29
+ if (tokens.length) {
29
30
  if (!rawConfig.resolve) rawConfig.resolve = {};
30
31
  if (!rawConfig.resolve.http) rawConfig.resolve.http = {};
31
- rawConfig.resolve.http.headers = [
32
- {
33
- matches: `https://api.${process.env.REDOCLY_DOMAIN || 'redoc.ly'}/registry/**`,
32
+ rawConfig.resolve.http.headers = [...(rawConfig.resolve.http.headers ?? [])];
33
+
34
+ for (const item of tokens) {
35
+ const domain = DOMAINS[item.region as Region];
36
+ rawConfig.resolve.http.headers.push({
37
+ matches: `https://api.${domain}/registry/**`,
34
38
  name: 'Authorization',
35
39
  envVariable: undefined,
36
- value: (redoclyClient && (await redoclyClient.getAuthorizationHeader())) || '',
40
+ value: item.token,
37
41
  },
38
- ...(rawConfig.resolve.http.headers ?? []),
39
- ];
42
+ //support redocly.com domain for future compatibility
43
+ ...(item.region === 'us' ? [{
44
+ matches: `https://api.redocly.com/registry/**`,
45
+ name: 'Authorization',
46
+ envVariable: undefined,
47
+ value: item.token,
48
+ }] : []));
49
+ }
40
50
  }
51
+
41
52
  return new Config(
42
53
  {
43
54
  ...rawConfig,
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export { StatsAccumulator, StatsName } from './typings/common';
17
17
  export { normalizeTypes } from './types';
18
18
  export { Stats } from './rules/other/stats';
19
19
 
20
- export { Config, LintConfig, RawConfig, IGNORE_FILE } from './config/config';
20
+ export { Config, LintConfig, RawConfig, IGNORE_FILE, Region } from './config/config';
21
21
  export { loadConfig } from './config/load';
22
22
  export { RedoclyClient } from './redocly';
23
23
  export {
@@ -46,11 +46,5 @@ export {
46
46
 
47
47
  export { getAstNodeByPointer, getLineColLocation } from './format/codeframes';
48
48
  export { formatProblems, OutputFormat, getTotals, Totals } from './format/format';
49
- export {
50
- lint,
51
- lint as validate,
52
- lintDocument,
53
- lintFromString,
54
- lintConfig,
55
- } from './lint';
49
+ export { lint, lint as validate, lintDocument, lintFromString, lintConfig } from './lint';
56
50
  export { bundle, bundleDocument } from './bundle';
@@ -0,0 +1,120 @@
1
+ import { RedoclyClient } from '../index';
2
+
3
+ describe('RedoclyClient', () => {
4
+ const REDOCLY_DOMAIN_US = 'redoc.ly';
5
+ const REDOCLY_DOMAIN_EU = 'eu.redocly.com';
6
+ const REDOCLY_AUTHORIZATION_TOKEN = 'redocly-auth-token';
7
+ const testRedoclyDomain = 'redoclyDomain.com';
8
+ const testToken = 'test-token';
9
+
10
+ afterEach(() => {
11
+ delete process.env.REDOCLY_DOMAIN;
12
+ });
13
+
14
+ it('should resolve the US domain by default', () => {
15
+ const client = new RedoclyClient();
16
+ expect(client.domain).toBe(REDOCLY_DOMAIN_US);
17
+ });
18
+
19
+ it('should resolve domain from RedoclyDomain env', () => {
20
+ process.env.REDOCLY_DOMAIN = testRedoclyDomain;
21
+ const client = new RedoclyClient();
22
+ expect(client.domain).toBe(testRedoclyDomain);
23
+ });
24
+
25
+ it('should resolve a domain by US region', () => {
26
+ const client = new RedoclyClient('us');
27
+ expect(client.domain).toBe(REDOCLY_DOMAIN_US);
28
+ });
29
+
30
+ it('should resolve a domain by EU region', () => {
31
+ const client = new RedoclyClient('eu');
32
+ expect(client.domain).toBe(REDOCLY_DOMAIN_EU);
33
+ });
34
+
35
+ it('should resolve domain by EU region prioritizing flag over env variable', () => {
36
+ process.env.REDOCLY_DOMAIN = testRedoclyDomain;
37
+ const client = new RedoclyClient('eu');
38
+ expect(client.domain).toBe(REDOCLY_DOMAIN_EU);
39
+ });
40
+
41
+ it('should resolve domain by US region prioritizing flag over env variable', () => {
42
+ process.env.REDOCLY_DOMAIN = testRedoclyDomain;
43
+ const client = new RedoclyClient('us');
44
+ expect(client.domain).toBe(REDOCLY_DOMAIN_US);
45
+ });
46
+
47
+ it('should resolve domain by US region when REDOCLY_DOMAIN consists EU domain', () => {
48
+ process.env.REDOCLY_DOMAIN = REDOCLY_DOMAIN_EU;
49
+ const client = new RedoclyClient();
50
+ expect(client.getRegion()).toBe('eu');
51
+ });
52
+
53
+ it('should resolve valid tokens data', async () => {
54
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => {
55
+ return { us: "accessToken", eu: "eu-accessToken" }
56
+ });
57
+ const client = new RedoclyClient();
58
+ const tokens = await client.getValidTokens();
59
+ expect(tokens).toStrictEqual([
60
+ { region: 'us', token: 'accessToken', valid: true },
61
+ { region: 'eu', token: 'eu-accessToken', valid: true }
62
+ ]);
63
+ spy.mockRestore();
64
+ });
65
+
66
+ it('should not call setAccessTokens by default', () => {
67
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({}));
68
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
69
+ const client = new RedoclyClient();
70
+ expect(client.setAccessTokens).not.toHaveBeenCalled()
71
+ spy.mockRestore();
72
+ });
73
+
74
+ it('should set correct accessTokens - backward compatibility: default US region', () => {
75
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({ token: testToken }));
76
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
77
+ const client = new RedoclyClient();
78
+ expect(client.setAccessTokens).toBeCalledWith(
79
+ expect.objectContaining({ us: testToken })
80
+ );
81
+ spy.mockRestore();
82
+ });
83
+
84
+ it('should set correct accessTokens - backward compatibility: EU region', () => {
85
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({ token: testToken }));
86
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
87
+ const client = new RedoclyClient('eu');
88
+ expect(client.setAccessTokens).toBeCalledWith(
89
+ expect.objectContaining({ eu: testToken })
90
+ );
91
+ spy.mockRestore();
92
+ });
93
+
94
+ it('should set correct accessTokens - REDOCLY_AUTHORIZATION env', () => {
95
+ process.env.REDOCLY_AUTHORIZATION = REDOCLY_AUTHORIZATION_TOKEN;
96
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation();
97
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
98
+ const client = new RedoclyClient();
99
+ expect(client.setAccessTokens).toHaveBeenNthCalledWith(1, { "us": REDOCLY_AUTHORIZATION_TOKEN });
100
+ spy.mockRestore();
101
+ });
102
+
103
+ it('should set correct accessTokens prioritizing REDOCLY_AUTHORIZATION env over token in file', () => {
104
+ process.env.REDOCLY_AUTHORIZATION = REDOCLY_AUTHORIZATION_TOKEN;
105
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({ token: testToken }));
106
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
107
+ const client = new RedoclyClient();
108
+ expect(client.setAccessTokens).toHaveBeenNthCalledWith(2, { "us": REDOCLY_AUTHORIZATION_TOKEN });
109
+ spy.mockRestore();
110
+ });
111
+
112
+ it('should set correct accessTokens prioritizing REDOCLY_AUTHORIZATION env over EU token', () => {
113
+ process.env.REDOCLY_AUTHORIZATION = REDOCLY_AUTHORIZATION_TOKEN;
114
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({ us: testToken }));
115
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
116
+ const client = new RedoclyClient('eu');
117
+ expect(client.setAccessTokens).toHaveBeenNthCalledWith(2, { "eu": REDOCLY_AUTHORIZATION_TOKEN });
118
+ spy.mockRestore();
119
+ });
120
+ });
@@ -1,50 +1,61 @@
1
1
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
2
2
  import { resolve } from 'path';
3
3
  import { homedir } from 'os';
4
- import { yellow, red, green, gray } from 'colorette';
4
+ import { red, green, gray, yellow } from 'colorette';
5
5
  import { RegistryApi } from './registry-api';
6
+ import { AccessTokens, DEFAULT_REGION, DOMAINS, Region } from '../config/config';
7
+ import { isNotEmptyObject } from '../utils';
6
8
 
7
9
  const TOKEN_FILENAME = '.redocly-config.json';
8
10
 
9
11
  export class RedoclyClient {
10
- private accessToken: string | undefined;
12
+ private accessTokens: AccessTokens = {};
13
+ private region: Region;
14
+ domain: string;
11
15
  registryApi: RegistryApi;
12
16
 
13
- constructor() {
14
- this.loadToken();
15
- this.registryApi = new RegistryApi(this.accessToken);
17
+ constructor(region?: Region) {
18
+ this.region = this.loadRegion(region);
19
+ this.loadTokens();
20
+ this.domain = region
21
+ ? DOMAINS[region]
22
+ : process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
23
+ this.registryApi = new RegistryApi(this.accessTokens, this.region);
16
24
  }
17
25
 
18
- hasToken(): boolean {
19
- return !!this.accessToken;
20
- }
21
-
22
- loadToken(): void {
23
- if (process.env.REDOCLY_AUTHORIZATION) {
24
- this.accessToken = process.env.REDOCLY_AUTHORIZATION;
25
- return;
26
+ loadRegion(region?: Region) {
27
+ if (region && !DOMAINS[region]) {
28
+ process.stdout.write(
29
+ red(`Invalid argument: region in config file.\nGiven: ${green(region)}, choices: "us", "eu".\n`),
30
+ );
31
+ process.exit(1);
26
32
  }
27
33
 
28
- const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
29
- if (existsSync(credentialsPath)) {
30
- const credentials = JSON.parse(readFileSync(credentialsPath, 'utf-8'));
31
- this.accessToken = credentials && credentials.token;
34
+ if (process.env.REDOCLY_DOMAIN) {
35
+ return (Object.keys(DOMAINS).find(
36
+ (region) => DOMAINS[region as Region] === process.env.REDOCLY_DOMAIN,
37
+ ) || DEFAULT_REGION) as Region;
32
38
  }
39
+ return region || DEFAULT_REGION;
33
40
  }
34
41
 
35
- async isAuthorizedWithRedocly(): Promise<boolean> {
36
- return this.hasToken() && !!(await this.getAuthorizationHeader());
42
+ getRegion(): Region {
43
+ return this.region;
37
44
  }
38
45
 
39
- async verifyToken(accessToken: string, verbose: boolean = false): Promise<boolean> {
40
- if (!accessToken) return false;
46
+ hasTokens(): boolean {
47
+ return isNotEmptyObject(this.accessTokens);
48
+ }
41
49
 
42
- return this.registryApi.setAccessToken(accessToken).authStatus(verbose);
50
+ // <backward compatibility: old versions of portal>
51
+ hasToken() {
52
+ return !!this.accessTokens[this.region];
43
53
  }
44
54
 
45
55
  async getAuthorizationHeader(): Promise<string | undefined> {
56
+ const token = this.accessTokens[this.region];
46
57
  // print this only if there is token but invalid
47
- if (this.accessToken && !(await this.verifyToken(this.accessToken))) {
58
+ if (token && !this.isAuthorizedWithRedoclyByRegion()) {
48
59
  process.stderr.write(
49
60
  `${yellow(
50
61
  'Warning:',
@@ -52,15 +63,74 @@ export class RedoclyClient {
52
63
  );
53
64
  return undefined;
54
65
  }
55
- return this.accessToken;
66
+
67
+ return token;
68
+ }
69
+ // </backward compatibility: portal>
70
+
71
+ setAccessTokens(accessTokens: AccessTokens) {
72
+ this.accessTokens = accessTokens;
73
+ }
74
+
75
+ loadTokens(): void {
76
+ const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
77
+ const credentials = this.readCredentialsFile(credentialsPath);
78
+ if (isNotEmptyObject(credentials)) {
79
+ this.setAccessTokens({
80
+ ...credentials,
81
+ ...(credentials.token && !credentials[this.region] && {
82
+ [this.region]: credentials.token
83
+ })
84
+ })
85
+ }
86
+ if (process.env.REDOCLY_AUTHORIZATION) {
87
+ this.setAccessTokens({
88
+ ...this.accessTokens,
89
+ [this.region]: process.env.REDOCLY_AUTHORIZATION
90
+ })
91
+ }
92
+ }
93
+
94
+ async getValidTokens(): Promise<{
95
+ region: string;
96
+ token: string;
97
+ valid: boolean;
98
+ }[]> {
99
+ return (await Promise.all(
100
+ Object.entries(this.accessTokens).map(async ([key, value]) => {
101
+ return { region: key, token: value, valid: await this.verifyToken(value, key as Region) }
102
+ })
103
+ )).filter(item => Boolean(item.valid));
104
+ }
105
+
106
+ async getTokens() {
107
+ return this.hasTokens() ? await this.getValidTokens() : [];
108
+ }
109
+
110
+ async isAuthorizedWithRedoclyByRegion(): Promise<boolean> {
111
+ if (!this.hasTokens()) return false;
112
+ const accessToken = this.accessTokens[this.region];
113
+ return !!accessToken && await this.verifyToken(accessToken, this.region);
114
+ }
115
+
116
+ async isAuthorizedWithRedocly(): Promise<boolean> {
117
+ return this.hasTokens() && isNotEmptyObject(await this.getValidTokens());
118
+ }
119
+
120
+ readCredentialsFile(credentialsPath: string) {
121
+ return existsSync(credentialsPath) ? JSON.parse(readFileSync(credentialsPath, 'utf-8')) : {};
122
+ }
123
+
124
+ async verifyToken(accessToken: string, region: Region, verbose: boolean = false): Promise<boolean> {
125
+ if (!accessToken) return false;
126
+ return this.registryApi.authStatus(accessToken, region, verbose);
56
127
  }
57
128
 
58
129
  async login(accessToken: string, verbose: boolean = false) {
59
130
  const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
60
131
  process.stdout.write(gray('\n Logging in...\n'));
61
132
 
62
- const authorized = await this.verifyToken(accessToken, verbose);
63
-
133
+ const authorized = await this.verifyToken(accessToken, this.region, verbose);
64
134
  if (!authorized) {
65
135
  process.stdout.write(
66
136
  red('Authorization failed. Please check if you entered a valid API key.\n'),
@@ -68,11 +138,13 @@ export class RedoclyClient {
68
138
  process.exit(1);
69
139
  }
70
140
 
71
- this.accessToken = accessToken;
72
141
  const credentials = {
73
- token: accessToken,
142
+ ...this.readCredentialsFile(credentialsPath),
143
+ [this.region!]: accessToken,
144
+ token: accessToken, // FIXME: backward compatibility, remove on 1.0.0
74
145
  };
75
-
146
+ this.accessTokens = credentials;
147
+ this.registryApi.setAccessTokens(credentials);
76
148
  writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
77
149
  process.stdout.write(green(' Authorization confirmed. ✅\n\n'));
78
150
  }
@@ -87,7 +159,7 @@ export class RedoclyClient {
87
159
  }
88
160
 
89
161
  export function isRedoclyRegistryURL(link: string): boolean {
90
- const domain = process.env.REDOCLY_DOMAIN || 'redoc.ly';
162
+ const domain = process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
91
163
  if (!link.startsWith(`https://api.${domain}/registry/`)) return false;
92
164
  const registryPath = link.replace(`https://api.${domain}/registry/`, '');
93
165
 
@@ -1,45 +1,40 @@
1
- import fetch, { RequestInit } from 'node-fetch';
1
+ import fetch, { RequestInit, HeadersInit } from 'node-fetch';
2
2
  import { RegistryApiTypes } from './registry-api-types';
3
+ import { AccessTokens, Region, DEFAULT_REGION, DOMAINS } from '../config/config';
4
+ import { isNotEmptyObject } from '../utils';
3
5
  const version = require('../../package.json').version;
4
6
 
5
7
  export class RegistryApi {
6
- private readonly baseUrl = `https://api.${process.env.REDOCLY_DOMAIN || 'redoc.ly'}/registry`;
8
+ constructor(private accessTokens: AccessTokens, private region: Region) {}
7
9
 
8
- constructor(private accessToken?: string) {}
9
-
10
- private async request(path = '', options: RequestInit = {}) {
11
- if (!this.accessToken) {
12
- throw new Error('Unauthorized');
13
- }
14
-
15
- const headers = Object.assign({}, options.headers || {}, {
16
- authorization: this.accessToken,
17
- 'x-redocly-cli-version': version,
18
- });
10
+ get accessToken() {
11
+ return isNotEmptyObject(this.accessTokens) && this.accessTokens[this.region];
12
+ }
19
13
 
20
- const response = await fetch(`${this.baseUrl}${path}`, Object.assign({}, options, { headers }));
14
+ getBaseUrl(region: Region = DEFAULT_REGION) {
15
+ return `https://api.${DOMAINS[region]}/registry`;
16
+ }
21
17
 
22
- if (response.status === 401) {
23
- throw new Error('Unauthorized');
24
- }
18
+ setAccessTokens(accessTokens: AccessTokens) {
19
+ this.accessTokens = accessTokens;
20
+ return this;
21
+ }
25
22
 
23
+ private async request(path = '', options: RequestInit = {}, region?: Region) {
24
+ const headers = Object.assign({}, options.headers || {}, { 'x-redocly-cli-version': version });
25
+ if (!headers.hasOwnProperty('authorization')) { throw new Error('Unauthorized'); }
26
+ const response = await fetch(`${this.getBaseUrl(region)}${path}`, Object.assign({}, options, { headers }));
27
+ if (response.status === 401) { throw new Error('Unauthorized'); }
26
28
  if (response.status === 404) {
27
29
  const body: RegistryApiTypes.NotFoundProblemResponse = await response.json();
28
30
  throw new Error(body.code);
29
31
  }
30
-
31
32
  return response;
32
33
  }
33
34
 
34
- setAccessToken(accessToken: string) {
35
- this.accessToken = accessToken;
36
- return this;
37
- }
38
-
39
- async authStatus(verbose = false) {
35
+ async authStatus(accessToken: string, region: Region, verbose = false) {
40
36
  try {
41
- const response = await this.request();
42
-
37
+ const response = await this.request('', { headers: { authorization: accessToken }}, region);
43
38
  return response.ok;
44
39
  } catch (error) {
45
40
  if (verbose) {
@@ -61,13 +56,17 @@ export class RegistryApi {
61
56
  `/${organizationId}/${name}/${version}/prepare-file-upload`,
62
57
  {
63
58
  method: 'POST',
64
- headers: { 'content-type': 'application/json' },
59
+ headers: {
60
+ 'content-type': 'application/json',
61
+ authorization: this.accessToken,
62
+ } as HeadersInit,
65
63
  body: JSON.stringify({
66
64
  filesHash,
67
65
  filename,
68
66
  isUpsert,
69
67
  }),
70
68
  },
69
+ this.region
71
70
  );
72
71
 
73
72
  if (response.ok) {
@@ -88,14 +87,19 @@ export class RegistryApi {
88
87
  }: RegistryApiTypes.PushApiParams) {
89
88
  const response = await this.request(`/${organizationId}/${name}/${version}`, {
90
89
  method: 'PUT',
91
- headers: { 'content-type': 'application/json' },
90
+ headers: {
91
+ 'content-type': 'application/json',
92
+ authorization: this.accessToken
93
+ } as HeadersInit,
92
94
  body: JSON.stringify({
93
95
  rootFilePath,
94
96
  filePaths,
95
97
  branch,
96
98
  isUpsert,
97
99
  }),
98
- });
100
+ },
101
+ this.region
102
+ );
99
103
 
100
104
  if (response.ok) {
101
105
  return;