@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.
- package/__tests__/lint.test.ts +1 -1
- package/__tests__/login.test.ts +17 -0
- package/lib/config/all.js +2 -0
- package/lib/config/config.d.ts +10 -0
- package/lib/config/config.js +12 -1
- package/lib/config/load.js +17 -8
- package/lib/index.d.ts +2 -2
- package/lib/redocly/index.d.ts +24 -5
- package/lib/redocly/index.js +82 -30
- package/lib/redocly/registry-api.d.ts +8 -5
- package/lib/redocly/registry-api.js +31 -20
- package/lib/rules/common/no-invalid-parameter-examples.d.ts +1 -0
- package/lib/rules/common/no-invalid-parameter-examples.js +25 -0
- package/lib/rules/common/no-invalid-schema-examples.d.ts +1 -0
- package/lib/rules/common/no-invalid-schema-examples.js +23 -0
- package/lib/rules/common/paths-kebab-case.js +1 -1
- package/lib/rules/oas2/index.d.ts +2 -0
- package/lib/rules/oas2/index.js +4 -0
- package/lib/rules/oas3/index.js +4 -0
- package/lib/rules/oas3/no-invalid-media-type-examples.js +5 -26
- package/lib/rules/utils.d.ts +3 -0
- package/lib/rules/utils.js +26 -1
- package/lib/typings/openapi.d.ts +3 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +5 -1
- package/package.json +2 -2
- package/src/config/__tests__/load.test.ts +35 -0
- package/src/config/all.ts +2 -0
- package/src/config/config.ts +17 -0
- package/src/config/load.ts +20 -9
- package/src/index.ts +2 -8
- package/src/redocly/__tests__/redocly-client.test.ts +120 -0
- package/src/redocly/index.ts +102 -30
- package/src/redocly/registry-api.ts +33 -29
- package/src/rules/common/__tests__/paths-kebab-case.test.ts +23 -0
- package/src/rules/common/no-invalid-parameter-examples.ts +36 -0
- package/src/rules/common/no-invalid-schema-examples.ts +27 -0
- package/src/rules/common/paths-kebab-case.ts +1 -1
- package/src/rules/oas2/index.ts +4 -0
- package/src/rules/oas3/index.ts +4 -0
- package/src/rules/oas3/no-invalid-media-type-examples.ts +16 -36
- package/src/rules/utils.ts +43 -2
- package/src/typings/openapi.ts +4 -0
- package/src/utils.ts +5 -1
- package/tsconfig.tsbuildinfo +1 -1
package/lib/rules/utils.js
CHANGED
|
@@ -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;
|
package/lib/typings/openapi.d.ts
CHANGED
package/lib/utils.d.ts
CHANGED
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
|
@@ -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
package/src/config/config.ts
CHANGED
|
@@ -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
|
|
package/src/config/load.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
40
|
+
value: item.token,
|
|
37
41
|
},
|
|
38
|
-
|
|
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
|
+
});
|
package/src/redocly/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
12
|
+
private accessTokens: AccessTokens = {};
|
|
13
|
+
private region: Region;
|
|
14
|
+
domain: string;
|
|
11
15
|
registryApi: RegistryApi;
|
|
12
16
|
|
|
13
|
-
constructor() {
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
-
return this.
|
|
42
|
+
getRegion(): Region {
|
|
43
|
+
return this.region;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
hasTokens(): boolean {
|
|
47
|
+
return isNotEmptyObject(this.accessTokens);
|
|
48
|
+
}
|
|
41
49
|
|
|
42
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
8
|
+
constructor(private accessTokens: AccessTokens, private region: Region) {}
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
+
getBaseUrl(region: Region = DEFAULT_REGION) {
|
|
15
|
+
return `https://api.${DOMAINS[region]}/registry`;
|
|
16
|
+
}
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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;
|