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

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 +7 -1
  6. package/lib/config/load.js +17 -8
  7. package/lib/index.d.ts +2 -2
  8. package/lib/redocly/index.d.ts +22 -6
  9. package/lib/redocly/index.js +61 -31
  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 +11 -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 +114 -0
  33. package/src/redocly/index.ts +77 -37
  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.70",
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,20 @@ 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
+ export const DOMAINS: { [region in Region]: string } = {
130
+ us: 'redoc.ly',
131
+ eu: 'eu.redocly.com',
132
+ };
133
+
126
134
  export type RawConfig = {
127
135
  referenceDocs?: any;
128
136
  apiDefinitions?: Record<string, string>;
129
137
  lint?: LintRawConfig;
130
138
  resolve?: RawResolveConfig;
139
+ region?: Region;
131
140
  };
132
141
 
133
142
  export class LintConfig {
@@ -385,6 +394,7 @@ export class Config {
385
394
  lint: LintConfig;
386
395
  resolve: ResolveConfig;
387
396
  licenseKey?: string;
397
+ region?: Region;
388
398
  constructor(public rawConfig: RawConfig, public configFile?: string) {
389
399
  this.apiDefinitions = rawConfig.apiDefinitions || {};
390
400
  this.lint = new LintConfig(rawConfig.lint || {}, configFile);
@@ -395,6 +405,7 @@ export class Config {
395
405
  customFetch: undefined,
396
406
  },
397
407
  };
408
+ this.region = rawConfig.region;
398
409
  }
399
410
  }
400
411
 
@@ -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,114 @@
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 valid tokens data', async () => {
48
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => {
49
+ return { us: "accessToken", eu: "eu-accessToken" }
50
+ });
51
+ const client = new RedoclyClient();
52
+ const tokens = await client.getValidTokens();
53
+ expect(tokens).toStrictEqual([
54
+ { region: 'us', token: 'accessToken', valid: true },
55
+ { region: 'eu', token: 'eu-accessToken', valid: true }
56
+ ]);
57
+ spy.mockRestore();
58
+ });
59
+
60
+ it('should not call setAccessTokens by default', () => {
61
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({}));
62
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
63
+ const client = new RedoclyClient();
64
+ expect(client.setAccessTokens).not.toHaveBeenCalled()
65
+ spy.mockRestore();
66
+ });
67
+
68
+ it('should set correct accessTokens - backward compatibility: default US region', () => {
69
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({ token: testToken }));
70
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
71
+ const client = new RedoclyClient();
72
+ expect(client.setAccessTokens).toBeCalledWith(
73
+ expect.objectContaining({ us: testToken })
74
+ );
75
+ spy.mockRestore();
76
+ });
77
+
78
+ it('should set correct accessTokens - backward compatibility: EU region', () => {
79
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({ token: testToken }));
80
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
81
+ const client = new RedoclyClient('eu');
82
+ expect(client.setAccessTokens).toBeCalledWith(
83
+ expect.objectContaining({ eu: testToken })
84
+ );
85
+ spy.mockRestore();
86
+ });
87
+
88
+ it('should set correct accessTokens - REDOCLY_AUTHORIZATION env', () => {
89
+ process.env.REDOCLY_AUTHORIZATION = REDOCLY_AUTHORIZATION_TOKEN;
90
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation();
91
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
92
+ const client = new RedoclyClient();
93
+ expect(client.setAccessTokens).toHaveBeenNthCalledWith(1, { "us": REDOCLY_AUTHORIZATION_TOKEN });
94
+ spy.mockRestore();
95
+ });
96
+
97
+ it('should set correct accessTokens prioritizing REDOCLY_AUTHORIZATION env over token in file', () => {
98
+ process.env.REDOCLY_AUTHORIZATION = REDOCLY_AUTHORIZATION_TOKEN;
99
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({ token: testToken }));
100
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
101
+ const client = new RedoclyClient();
102
+ expect(client.setAccessTokens).toHaveBeenNthCalledWith(2, { "us": REDOCLY_AUTHORIZATION_TOKEN });
103
+ spy.mockRestore();
104
+ });
105
+
106
+ it('should set correct accessTokens prioritizing REDOCLY_AUTHORIZATION env over EU token', () => {
107
+ process.env.REDOCLY_AUTHORIZATION = REDOCLY_AUTHORIZATION_TOKEN;
108
+ let spy = jest.spyOn(RedoclyClient.prototype, 'readCredentialsFile').mockImplementation(() => ({ us: testToken }));
109
+ jest.spyOn(RedoclyClient.prototype, 'setAccessTokens').mockImplementation();
110
+ const client = new RedoclyClient('eu');
111
+ expect(client.setAccessTokens).toHaveBeenNthCalledWith(2, { "eu": REDOCLY_AUTHORIZATION_TOKEN });
112
+ spy.mockRestore();
113
+ });
114
+ });
@@ -1,66 +1,105 @@
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 } 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;
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);
32
+ }
33
+ return region || DEFAULT_REGION;
20
34
  }
21
35
 
22
- loadToken(): void {
23
- if (process.env.REDOCLY_AUTHORIZATION) {
24
- this.accessToken = process.env.REDOCLY_AUTHORIZATION;
25
- return;
26
- }
36
+ hasTokens(): boolean {
37
+ return isNotEmptyObject(this.accessTokens);
38
+ }
27
39
 
40
+ setAccessTokens(accessTokens: AccessTokens) {
41
+ this.accessTokens = accessTokens;
42
+ }
43
+
44
+ loadTokens(): void {
28
45
  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;
46
+ const credentials = this.readCredentialsFile(credentialsPath);
47
+ if (isNotEmptyObject(credentials)) {
48
+ this.setAccessTokens({
49
+ ...credentials,
50
+ ...(credentials.token && !credentials[this.region] && {
51
+ [this.region]: credentials.token
52
+ })
53
+ })
54
+ }
55
+ if (process.env.REDOCLY_AUTHORIZATION) {
56
+ this.setAccessTokens({
57
+ ...this.accessTokens,
58
+ [this.region]: process.env.REDOCLY_AUTHORIZATION
59
+ })
32
60
  }
33
61
  }
34
62
 
35
- async isAuthorizedWithRedocly(): Promise<boolean> {
36
- return this.hasToken() && !!(await this.getAuthorizationHeader());
63
+ async getValidTokens(): Promise<{
64
+ region: string;
65
+ token: string;
66
+ valid: boolean;
67
+ }[]> {
68
+ return (await Promise.all(
69
+ Object.entries(this.accessTokens).map(async ([key, value]) => {
70
+ return { region: key, token: value, valid: await this.verifyToken(value, key as Region) }
71
+ })
72
+ )).filter(item => Boolean(item.valid));
37
73
  }
38
74
 
39
- async verifyToken(accessToken: string, verbose: boolean = false): Promise<boolean> {
40
- if (!accessToken) return false;
75
+ async getTokens() {
76
+ return this.hasTokens() ? await this.getValidTokens() : [];
77
+ }
41
78
 
42
- return this.registryApi.setAccessToken(accessToken).authStatus(verbose);
79
+ async isAuthorizedWithRedoclyByRegion(): Promise<boolean> {
80
+ if (!this.hasTokens()) return false;
81
+ const accessToken = this.accessTokens[this.region];
82
+ return !!accessToken && await this.verifyToken(accessToken, this.region);
43
83
  }
44
84
 
45
- async getAuthorizationHeader(): Promise<string | undefined> {
46
- // print this only if there is token but invalid
47
- if (this.accessToken && !(await this.verifyToken(this.accessToken))) {
48
- process.stderr.write(
49
- `${yellow(
50
- 'Warning:',
51
- )} invalid Redocly API key. Use "npx @redocly/openapi-cli login" to provide your API key\n`,
52
- );
53
- return undefined;
54
- }
55
- return this.accessToken;
85
+ async isAuthorizedWithRedocly(): Promise<boolean> {
86
+ return this.hasTokens() && isNotEmptyObject(await this.getValidTokens());
87
+ }
88
+
89
+ readCredentialsFile(credentialsPath: string) {
90
+ return existsSync(credentialsPath) ? JSON.parse(readFileSync(credentialsPath, 'utf-8')) : {};
91
+ }
92
+
93
+ async verifyToken(accessToken: string, region: Region, verbose: boolean = false): Promise<boolean> {
94
+ if (!accessToken) return false;
95
+ return this.registryApi.authStatus(accessToken, region, verbose);
56
96
  }
57
97
 
58
98
  async login(accessToken: string, verbose: boolean = false) {
59
99
  const credentialsPath = resolve(homedir(), TOKEN_FILENAME);
60
100
  process.stdout.write(gray('\n Logging in...\n'));
61
101
 
62
- const authorized = await this.verifyToken(accessToken, verbose);
63
-
102
+ const authorized = await this.verifyToken(accessToken, this.region, verbose);
64
103
  if (!authorized) {
65
104
  process.stdout.write(
66
105
  red('Authorization failed. Please check if you entered a valid API key.\n'),
@@ -68,11 +107,12 @@ export class RedoclyClient {
68
107
  process.exit(1);
69
108
  }
70
109
 
71
- this.accessToken = accessToken;
72
110
  const credentials = {
73
- token: accessToken,
111
+ ...this.readCredentialsFile(credentialsPath),
112
+ [this.region!]: accessToken,
74
113
  };
75
-
114
+ this.accessTokens = credentials;
115
+ this.registryApi.setAccessTokens(credentials);
76
116
  writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
77
117
  process.stdout.write(green(' Authorization confirmed. ✅\n\n'));
78
118
  }
@@ -87,7 +127,7 @@ export class RedoclyClient {
87
127
  }
88
128
 
89
129
  export function isRedoclyRegistryURL(link: string): boolean {
90
- const domain = process.env.REDOCLY_DOMAIN || 'redoc.ly';
130
+ const domain = process.env.REDOCLY_DOMAIN || DOMAINS[DEFAULT_REGION];
91
131
  if (!link.startsWith(`https://api.${domain}/registry/`)) return false;
92
132
  const registryPath = link.replace(`https://api.${domain}/registry/`, '');
93
133
 
@@ -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;
@@ -83,4 +83,27 @@ describe('Oas3 paths-kebab-case', () => {
83
83
  ]
84
84
  `);
85
85
  });
86
+
87
+ it('should allow trailing slash in path with "paths-kebab-case" rule', async () => {
88
+ const document = parseYamlToDocument(
89
+ outdent`
90
+ openapi: 3.0.0
91
+ paths:
92
+ /some/:
93
+ get:
94
+ summary: List all pets
95
+ `,
96
+ 'foobar.yaml',
97
+ );
98
+
99
+ const results = await lintDocument({
100
+ externalRefResolver: new BaseResolver(),
101
+ document,
102
+ config: makeConfig({
103
+ 'paths-kebab-case': 'error',
104
+ 'no-path-trailing-slash': 'off',
105
+ }),
106
+ });
107
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
108
+ });
86
109
  });