@redocly/cli 1.28.5 → 1.30.0

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 (90) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/lib/__tests__/commands/push-region.test.js +3 -3
  3. package/lib/__tests__/utils.test.js +1 -0
  4. package/lib/auth/__tests__/device-flow.test.js +62 -0
  5. package/lib/auth/__tests__/oauth-client.test.js +93 -0
  6. package/lib/auth/device-flow.d.ts +26 -0
  7. package/lib/auth/device-flow.js +133 -0
  8. package/lib/auth/oauth-client.d.ts +14 -0
  9. package/lib/auth/oauth-client.js +93 -0
  10. package/lib/commands/auth.d.ts +13 -0
  11. package/lib/commands/auth.js +51 -0
  12. package/lib/commands/push.d.ts +1 -1
  13. package/lib/commands/push.js +4 -4
  14. package/lib/commands/split/index.js +4 -4
  15. package/lib/index.js +14 -15
  16. package/lib/otel.d.ts +10 -0
  17. package/lib/otel.js +47 -0
  18. package/lib/reunite/api/__tests__/domains.test.js +32 -0
  19. package/lib/{cms → reunite}/api/api-client.d.ts +9 -0
  20. package/lib/{cms → reunite}/api/api-client.js +2 -1
  21. package/lib/reunite/api/domains.d.ts +4 -0
  22. package/lib/reunite/api/domains.js +22 -0
  23. package/lib/reunite/commands/__tests__/push.test.d.ts +1 -0
  24. package/lib/reunite/commands/__tests__/utils.test.d.ts +1 -0
  25. package/lib/types.d.ts +4 -4
  26. package/lib/utils/miscellaneous.d.ts +5 -4
  27. package/lib/utils/miscellaneous.js +14 -14
  28. package/package.json +7 -2
  29. package/src/__tests__/commands/push-region.test.ts +2 -2
  30. package/src/__tests__/utils.test.ts +1 -0
  31. package/src/auth/__tests__/device-flow.test.ts +73 -0
  32. package/src/auth/__tests__/oauth-client.test.ts +117 -0
  33. package/src/auth/device-flow.ts +175 -0
  34. package/src/auth/oauth-client.ts +111 -0
  35. package/src/commands/auth.ts +66 -0
  36. package/src/commands/push.ts +3 -3
  37. package/src/commands/split/index.ts +9 -9
  38. package/src/index.ts +14 -15
  39. package/src/otel.ts +59 -0
  40. package/src/reunite/api/__tests__/domains.test.ts +41 -0
  41. package/src/{cms → reunite}/api/api-client.ts +1 -1
  42. package/src/reunite/api/domains.ts +23 -0
  43. package/src/types.ts +4 -3
  44. package/src/utils/miscellaneous.ts +20 -18
  45. package/tsconfig.tsbuildinfo +1 -1
  46. package/lib/cms/api/__tests__/domains.test.js +0 -13
  47. package/lib/cms/api/domains.d.ts +0 -1
  48. package/lib/cms/api/domains.js +0 -11
  49. package/lib/commands/login.d.ts +0 -9
  50. package/lib/commands/login.js +0 -23
  51. package/src/cms/api/__tests__/domains.test.ts +0 -15
  52. package/src/cms/api/domains.ts +0 -11
  53. package/src/commands/login.ts +0 -34
  54. /package/lib/{cms/api/__tests__/api-keys.test.d.ts → auth/__tests__/device-flow.test.d.ts} +0 -0
  55. /package/lib/{cms/api/__tests__/api.client.test.d.ts → auth/__tests__/oauth-client.test.d.ts} +0 -0
  56. /package/lib/{cms/api/__tests__/domains.test.d.ts → reunite/api/__tests__/api-keys.test.d.ts} +0 -0
  57. /package/lib/{cms → reunite}/api/__tests__/api-keys.test.js +0 -0
  58. /package/lib/{cms/commands/__tests__/push-status.test.d.ts → reunite/api/__tests__/api.client.test.d.ts} +0 -0
  59. /package/lib/{cms → reunite}/api/__tests__/api.client.test.js +0 -0
  60. /package/lib/{cms/commands/__tests__/push.test.d.ts → reunite/api/__tests__/domains.test.d.ts} +0 -0
  61. /package/lib/{cms → reunite}/api/api-keys.d.ts +0 -0
  62. /package/lib/{cms → reunite}/api/api-keys.js +0 -0
  63. /package/lib/{cms → reunite}/api/index.d.ts +0 -0
  64. /package/lib/{cms → reunite}/api/index.js +0 -0
  65. /package/lib/{cms → reunite}/api/types.d.ts +0 -0
  66. /package/lib/{cms → reunite}/api/types.js +0 -0
  67. /package/lib/{cms/commands/__tests__/utils.test.d.ts → reunite/commands/__tests__/push-status.test.d.ts} +0 -0
  68. /package/lib/{cms → reunite}/commands/__tests__/push-status.test.js +0 -0
  69. /package/lib/{cms → reunite}/commands/__tests__/push.test.js +0 -0
  70. /package/lib/{cms → reunite}/commands/__tests__/utils.test.js +0 -0
  71. /package/lib/{cms → reunite}/commands/push-status.d.ts +0 -0
  72. /package/lib/{cms → reunite}/commands/push-status.js +0 -0
  73. /package/lib/{cms → reunite}/commands/push.d.ts +0 -0
  74. /package/lib/{cms → reunite}/commands/push.js +0 -0
  75. /package/lib/{cms → reunite}/commands/utils.d.ts +0 -0
  76. /package/lib/{cms → reunite}/commands/utils.js +0 -0
  77. /package/lib/{cms → reunite}/utils.d.ts +0 -0
  78. /package/lib/{cms → reunite}/utils.js +0 -0
  79. /package/src/{cms → reunite}/api/__tests__/api-keys.test.ts +0 -0
  80. /package/src/{cms → reunite}/api/__tests__/api.client.test.ts +0 -0
  81. /package/src/{cms → reunite}/api/api-keys.ts +0 -0
  82. /package/src/{cms → reunite}/api/index.ts +0 -0
  83. /package/src/{cms → reunite}/api/types.ts +0 -0
  84. /package/src/{cms → reunite}/commands/__tests__/push-status.test.ts +0 -0
  85. /package/src/{cms → reunite}/commands/__tests__/push.test.ts +0 -0
  86. /package/src/{cms → reunite}/commands/__tests__/utils.test.ts +0 -0
  87. /package/src/{cms → reunite}/commands/push-status.ts +0 -0
  88. /package/src/{cms → reunite}/commands/push.ts +0 -0
  89. /package/src/{cms → reunite}/commands/utils.ts +0 -0
  90. /package/src/{cms → reunite}/utils.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @redocly/cli
2
2
 
3
+ ## 1.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added [OAuth 2.0 Device authorization flow](https://datatracker.ietf.org/doc/html/rfc8628) that enables users to authenticate through Reunite API.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated `operation-tag-defined` built-in rule to verify tags are defined on the operation prior to matching them to a global tag.
12
+ - Updated @redocly/openapi-core to v1.30.0.
13
+
14
+ ## 1.29.0
15
+
16
+ ### Minor Changes
17
+
18
+ - Added typings and interfaces for Overlay Specification v1.0.0.
19
+
20
+ ### Patch Changes
21
+
22
+ - Fixed a problem where the `split` command produced backslashes instead of forward slashes in `$ref`s on Windows.
23
+ - Fixed an issue where the `no-invalid-media-type-examples` rule crashed instead of reporting an error when it failed to resolve an example from a $ref.
24
+ - Updated @redocly/openapi-core to v1.29.0.
25
+
3
26
  ## 1.28.5
4
27
 
5
28
  ### Patch Changes
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const openapi_core_1 = require("@redocly/openapi-core");
4
4
  const push_1 = require("../../commands/push");
5
- const login_1 = require("../../commands/login");
5
+ const auth_1 = require("../../commands/auth");
6
6
  const config_1 = require("../fixtures/config");
7
7
  const node_stream_1 = require("node:stream");
8
8
  // Mock fs operations
@@ -22,9 +22,9 @@ jest.mock('fs', () => ({
22
22
  openapi_core_1.getMergedConfig.mockImplementation((config) => config);
23
23
  // Mock OpenAPI core
24
24
  jest.mock('@redocly/openapi-core');
25
- jest.mock('../../commands/login');
25
+ jest.mock('../../commands/auth');
26
26
  jest.mock('../../utils/miscellaneous');
27
- const mockPromptClientToken = login_1.promptClientToken;
27
+ const mockPromptClientToken = auth_1.promptClientToken;
28
28
  describe('push-with-region', () => {
29
29
  const redoclyClient = require('@redocly/openapi-core').__redoclyClient;
30
30
  redoclyClient.isAuthorizedWithRedoclyByRegion = jest.fn().mockResolvedValue(false);
@@ -409,6 +409,7 @@ describe('checkIfRulesetExist', () => {
409
409
  async2: {},
410
410
  async3: {},
411
411
  arazzo1: {},
412
+ overlay1: {},
412
413
  };
413
414
  expect(() => (0, miscellaneous_1.checkIfRulesetExist)(rules)).toThrowError('⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/');
414
415
  });
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const device_flow_1 = require("../device-flow");
4
+ jest.mock('child_process');
5
+ describe('RedoclyOAuthDeviceFlow', () => {
6
+ const mockBaseUrl = 'https://test.redocly.com';
7
+ const mockClientName = 'test-client';
8
+ const mockVersion = '1.0.0';
9
+ let flow;
10
+ beforeEach(() => {
11
+ flow = new device_flow_1.RedoclyOAuthDeviceFlow(mockBaseUrl, mockClientName, mockVersion);
12
+ jest.resetAllMocks();
13
+ });
14
+ describe('verifyToken', () => {
15
+ it('returns true for valid token', async () => {
16
+ jest.spyOn(flow['apiClient'], 'request').mockResolvedValue({
17
+ json: () => Promise.resolve({ user: { id: '123' } }),
18
+ });
19
+ const result = await flow.verifyToken('valid-token');
20
+ expect(result).toBe(true);
21
+ });
22
+ it('returns false for invalid token', async () => {
23
+ jest.spyOn(flow['apiClient'], 'request').mockRejectedValue(new Error('Invalid token'));
24
+ const result = await flow.verifyToken('invalid-token');
25
+ expect(result).toBe(false);
26
+ });
27
+ });
28
+ describe('verifyApiKey', () => {
29
+ it('returns true for valid API key', async () => {
30
+ jest.spyOn(flow['apiClient'], 'request').mockResolvedValue({
31
+ json: () => Promise.resolve({ success: true }),
32
+ });
33
+ const result = await flow.verifyApiKey('valid-key');
34
+ expect(result).toBe(true);
35
+ });
36
+ it('returns false for invalid API key', async () => {
37
+ jest.spyOn(flow['apiClient'], 'request').mockRejectedValue(new Error('Invalid API key'));
38
+ const result = await flow.verifyApiKey('invalid-key');
39
+ expect(result).toBe(false);
40
+ });
41
+ });
42
+ describe('refreshToken', () => {
43
+ it('successfully refreshes token', async () => {
44
+ const mockResponse = {
45
+ access_token: 'new-token',
46
+ refresh_token: 'new-refresh',
47
+ expires_in: 3600,
48
+ };
49
+ jest.spyOn(flow['apiClient'], 'request').mockResolvedValue({
50
+ json: () => Promise.resolve(mockResponse),
51
+ });
52
+ const result = await flow.refreshToken('old-refresh-token');
53
+ expect(result).toEqual(mockResponse);
54
+ });
55
+ it('throws error when refresh fails', async () => {
56
+ jest.spyOn(flow['apiClient'], 'request').mockResolvedValue({
57
+ json: () => Promise.resolve({}),
58
+ });
59
+ await expect(flow.refreshToken('invalid-refresh')).rejects.toThrow('Failed to refresh token');
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const oauth_client_1 = require("../oauth-client");
4
+ const device_flow_1 = require("../device-flow");
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const os = require("node:os");
8
+ jest.mock('node:fs');
9
+ jest.mock('node:os');
10
+ jest.mock('../device-flow');
11
+ describe('RedoclyOAuthClient', () => {
12
+ const mockClientName = 'test-client';
13
+ const mockVersion = '1.0.0';
14
+ const mockBaseUrl = 'https://test.redocly.com';
15
+ const mockHomeDir = '/mock/home/dir';
16
+ const mockRedoclyDir = path.join(mockHomeDir, '.redocly');
17
+ let client;
18
+ beforeEach(() => {
19
+ jest.resetAllMocks();
20
+ os.homedir.mockReturnValue(mockHomeDir);
21
+ process.env.HOME = mockHomeDir;
22
+ client = new oauth_client_1.RedoclyOAuthClient(mockClientName, mockVersion);
23
+ });
24
+ describe('login', () => {
25
+ it('successfully logs in and saves token', async () => {
26
+ const mockToken = { access_token: 'test-token' };
27
+ const mockDeviceFlow = {
28
+ run: jest.fn().mockResolvedValue(mockToken),
29
+ };
30
+ device_flow_1.RedoclyOAuthDeviceFlow.mockImplementation(() => mockDeviceFlow);
31
+ await client.login(mockBaseUrl);
32
+ expect(mockDeviceFlow.run).toHaveBeenCalled();
33
+ expect(fs.writeFileSync).toHaveBeenCalled();
34
+ });
35
+ it('throws error when login fails', async () => {
36
+ const mockDeviceFlow = {
37
+ run: jest.fn().mockResolvedValue(null),
38
+ };
39
+ device_flow_1.RedoclyOAuthDeviceFlow.mockImplementation(() => mockDeviceFlow);
40
+ await expect(client.login(mockBaseUrl)).rejects.toThrow('Failed to login');
41
+ });
42
+ });
43
+ describe('logout', () => {
44
+ it('removes token file if it exists', async () => {
45
+ fs.existsSync.mockReturnValue(true);
46
+ await client.logout();
47
+ expect(fs.rmSync).toHaveBeenCalledWith(path.join(mockRedoclyDir, 'auth.json'));
48
+ });
49
+ it('silently fails if token file does not exist', async () => {
50
+ fs.existsSync.mockReturnValue(false);
51
+ await expect(client.logout()).resolves.not.toThrow();
52
+ expect(fs.rmSync).not.toHaveBeenCalled();
53
+ });
54
+ });
55
+ describe('isAuthorized', () => {
56
+ it('verifies API key if provided', async () => {
57
+ const mockDeviceFlow = {
58
+ verifyApiKey: jest.fn().mockResolvedValue(true),
59
+ };
60
+ device_flow_1.RedoclyOAuthDeviceFlow.mockImplementation(() => mockDeviceFlow);
61
+ const result = await client.isAuthorized(mockBaseUrl, 'test-api-key');
62
+ expect(result).toBe(true);
63
+ expect(mockDeviceFlow.verifyApiKey).toHaveBeenCalledWith('test-api-key');
64
+ });
65
+ it('verifies access token if no API key provided', async () => {
66
+ const mockToken = { access_token: 'test-token' };
67
+ const mockDeviceFlow = {
68
+ verifyToken: jest.fn().mockResolvedValue(true),
69
+ };
70
+ device_flow_1.RedoclyOAuthDeviceFlow.mockImplementation(() => mockDeviceFlow);
71
+ fs.readFileSync.mockReturnValue(client['cipher'].update(JSON.stringify(mockToken), 'utf8', 'hex') +
72
+ client['cipher'].final('hex'));
73
+ const result = await client.isAuthorized(mockBaseUrl);
74
+ expect(result).toBe(true);
75
+ expect(mockDeviceFlow.verifyToken).toHaveBeenCalledWith('test-token');
76
+ });
77
+ it('returns false if token refresh fails', async () => {
78
+ const mockToken = {
79
+ access_token: 'old-token',
80
+ refresh_token: 'refresh-token',
81
+ };
82
+ const mockDeviceFlow = {
83
+ verifyToken: jest.fn().mockResolvedValue(false),
84
+ refreshToken: jest.fn().mockRejectedValue(new Error('Refresh failed')),
85
+ };
86
+ device_flow_1.RedoclyOAuthDeviceFlow.mockImplementation(() => mockDeviceFlow);
87
+ fs.readFileSync.mockReturnValue(client['cipher'].update(JSON.stringify(mockToken), 'utf8', 'hex') +
88
+ client['cipher'].final('hex'));
89
+ const result = await client.isAuthorized(mockBaseUrl);
90
+ expect(result).toBe(false);
91
+ });
92
+ });
93
+ });
@@ -0,0 +1,26 @@
1
+ export type AuthToken = {
2
+ access_token: string;
3
+ refresh_token?: string;
4
+ token_type?: string;
5
+ expires_in?: number;
6
+ };
7
+ export declare class RedoclyOAuthDeviceFlow {
8
+ private baseUrl;
9
+ private clientName;
10
+ private version;
11
+ private apiClient;
12
+ constructor(baseUrl: string, clientName: string, version: string);
13
+ run(): Promise<AuthToken>;
14
+ private openBrowser;
15
+ verifyToken(accessToken: string): Promise<boolean>;
16
+ verifyApiKey(apiKey: string): Promise<boolean>;
17
+ refreshToken(refreshToken: string): Promise<{
18
+ access_token: any;
19
+ refresh_token: any;
20
+ expires_in: any;
21
+ }>;
22
+ private pollingAccessToken;
23
+ private getAccessToken;
24
+ private getDeviceCode;
25
+ private sendRequest;
26
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedoclyOAuthDeviceFlow = void 0;
4
+ const colorette_1 = require("colorette");
5
+ const childProcess = require("child_process");
6
+ const api_client_1 = require("../reunite/api/api-client");
7
+ class RedoclyOAuthDeviceFlow {
8
+ constructor(baseUrl, clientName, version) {
9
+ this.baseUrl = baseUrl;
10
+ this.clientName = clientName;
11
+ this.version = version;
12
+ this.apiClient = new api_client_1.ReuniteApiClient(this.version, 'login');
13
+ }
14
+ async run() {
15
+ const code = await this.getDeviceCode();
16
+ process.stdout.write('Attempting to automatically open the SSO authorization page in your default browser.\n');
17
+ process.stdout.write('If the browser does not open or you wish to use a different device to authorize this request, open the following URL:\n\n');
18
+ process.stdout.write((0, colorette_1.blue)(code.verificationUri));
19
+ process.stdout.write(`\n\n`);
20
+ process.stdout.write(`Then enter the code:\n\n`);
21
+ process.stdout.write((0, colorette_1.blue)(code.userCode));
22
+ process.stdout.write(`\n\n`);
23
+ this.openBrowser(code.verificationUriComplete);
24
+ const accessToken = await this.pollingAccessToken(code.deviceCode, code.interval, code.expiresIn);
25
+ process.stdout.write((0, colorette_1.green)('✅ Logged in\n\n'));
26
+ return accessToken;
27
+ }
28
+ openBrowser(url) {
29
+ try {
30
+ const cmd = process.platform === 'win32'
31
+ ? `start ${url}`
32
+ : process.platform === 'darwin'
33
+ ? `open ${url}`
34
+ : `xdg-open ${url}`;
35
+ childProcess.execSync(cmd);
36
+ }
37
+ catch {
38
+ // silently fail if browser cannot be opened
39
+ }
40
+ }
41
+ async verifyToken(accessToken) {
42
+ try {
43
+ const response = await this.sendRequest('/session', 'GET', undefined, {
44
+ Cookie: `accessToken=${accessToken};`,
45
+ });
46
+ return !!response.user;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ async verifyApiKey(apiKey) {
53
+ try {
54
+ const response = await this.sendRequest('/api-keys-verify', 'POST', {
55
+ apiKey,
56
+ });
57
+ return !!response.success;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ async refreshToken(refreshToken) {
64
+ const response = await this.sendRequest(`/device-rotate-token`, 'POST', {
65
+ grant_type: 'refresh_token',
66
+ client_name: this.clientName,
67
+ refresh_token: refreshToken,
68
+ });
69
+ if (!response.access_token) {
70
+ throw new Error('Failed to refresh token');
71
+ }
72
+ return {
73
+ access_token: response.access_token,
74
+ refresh_token: response.refresh_token,
75
+ expires_in: response.expires_in,
76
+ };
77
+ }
78
+ async pollingAccessToken(deviceCode, interval, expiresIn) {
79
+ return new Promise((resolve, reject) => {
80
+ const intervalId = setInterval(async () => {
81
+ const response = await this.getAccessToken(deviceCode);
82
+ if (response.access_token) {
83
+ clearInterval(intervalId);
84
+ clearTimeout(timeoutId);
85
+ resolve(response);
86
+ }
87
+ if (response.error && response.error !== 'authorization_pending') {
88
+ clearInterval(intervalId);
89
+ clearTimeout(timeoutId);
90
+ reject(response.error_description);
91
+ }
92
+ }, interval * 1000);
93
+ const timeoutId = setTimeout(async () => {
94
+ clearInterval(intervalId);
95
+ clearTimeout(timeoutId);
96
+ reject('Authorization has expired. Please try again.');
97
+ }, expiresIn * 1000);
98
+ });
99
+ }
100
+ async getAccessToken(deviceCode) {
101
+ return await this.sendRequest('/device-token', 'POST', {
102
+ client_name: this.clientName,
103
+ device_code: deviceCode,
104
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
105
+ });
106
+ }
107
+ async getDeviceCode() {
108
+ const { device_code: deviceCode, user_code: userCode, verification_uri: verificationUri, verification_uri_complete: verificationUriComplete, interval = 10, expires_in: expiresIn = 300, } = await this.sendRequest('/device-authorize', 'POST', {
109
+ client_name: this.clientName,
110
+ });
111
+ return {
112
+ deviceCode,
113
+ userCode,
114
+ verificationUri,
115
+ verificationUriComplete,
116
+ interval,
117
+ expiresIn,
118
+ };
119
+ }
120
+ async sendRequest(url, method = 'GET', body = undefined, headers = {}) {
121
+ url = `${this.baseUrl}${url}`;
122
+ const response = await this.apiClient.request(url, {
123
+ body: body ? JSON.stringify(body) : body,
124
+ method,
125
+ headers: { 'Content-Type': 'application/json', ...headers },
126
+ });
127
+ if (response.status === 204) {
128
+ return { success: true };
129
+ }
130
+ return await response.json();
131
+ }
132
+ }
133
+ exports.RedoclyOAuthDeviceFlow = RedoclyOAuthDeviceFlow;
@@ -0,0 +1,14 @@
1
+ export declare class RedoclyOAuthClient {
2
+ private clientName;
3
+ private version;
4
+ private dir;
5
+ private cipher;
6
+ private decipher;
7
+ constructor(clientName: string, version: string);
8
+ login(baseUrl: string): Promise<void>;
9
+ logout(): Promise<void>;
10
+ isAuthorized(baseUrl: string, apiKey?: string): Promise<boolean>;
11
+ private saveToken;
12
+ private readToken;
13
+ private removeToken;
14
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedoclyOAuthClient = void 0;
4
+ const node_os_1 = require("node:os");
5
+ const path = require("node:path");
6
+ const node_fs_1 = require("node:fs");
7
+ const crypto = require("node:crypto");
8
+ const node_buffer_1 = require("node:buffer");
9
+ const device_flow_1 = require("./device-flow");
10
+ const SALT = '4618dbc9-8aed-4e27-aaf0-225f4603e5a4';
11
+ const CRYPTO_ALGORITHM = 'aes-256-cbc';
12
+ class RedoclyOAuthClient {
13
+ constructor(clientName, version) {
14
+ this.clientName = clientName;
15
+ this.version = version;
16
+ this.dir = path.join((0, node_os_1.homedir)(), '.redocly');
17
+ if (!(0, node_fs_1.existsSync)(this.dir)) {
18
+ (0, node_fs_1.mkdirSync)(this.dir);
19
+ }
20
+ const homeDirPath = process.env.HOME;
21
+ const hash = crypto.createHash('sha256');
22
+ hash.update(`${homeDirPath}${SALT}`);
23
+ const hashHex = hash.digest('hex');
24
+ const key = node_buffer_1.Buffer.alloc(32, node_buffer_1.Buffer.from(hashHex).toString('base64')).toString();
25
+ const iv = node_buffer_1.Buffer.alloc(16, node_buffer_1.Buffer.from(process.env.HOME).toString('base64')).toString();
26
+ this.cipher = crypto.createCipheriv(CRYPTO_ALGORITHM, key, iv);
27
+ this.decipher = crypto.createDecipheriv(CRYPTO_ALGORITHM, key, iv);
28
+ }
29
+ async login(baseUrl) {
30
+ const deviceFlow = new device_flow_1.RedoclyOAuthDeviceFlow(baseUrl, this.clientName, this.version);
31
+ const token = await deviceFlow.run();
32
+ if (!token) {
33
+ throw new Error('Failed to login');
34
+ }
35
+ this.saveToken(token);
36
+ }
37
+ async logout() {
38
+ try {
39
+ this.removeToken();
40
+ }
41
+ catch (err) {
42
+ // do nothing
43
+ }
44
+ }
45
+ async isAuthorized(baseUrl, apiKey) {
46
+ const deviceFlow = new device_flow_1.RedoclyOAuthDeviceFlow(baseUrl, this.clientName, this.version);
47
+ if (apiKey) {
48
+ return await deviceFlow.verifyApiKey(apiKey);
49
+ }
50
+ const token = await this.readToken();
51
+ if (!token) {
52
+ return false;
53
+ }
54
+ const isValidAccessToken = await deviceFlow.verifyToken(token.access_token);
55
+ if (isValidAccessToken) {
56
+ return true;
57
+ }
58
+ try {
59
+ const newToken = await deviceFlow.refreshToken(token.refresh_token);
60
+ await this.saveToken(newToken);
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ return true;
66
+ }
67
+ async saveToken(token) {
68
+ try {
69
+ const encrypted = this.cipher.update(JSON.stringify(token), 'utf8', 'hex') + this.cipher.final('hex');
70
+ (0, node_fs_1.writeFileSync)(path.join(this.dir, 'auth.json'), encrypted);
71
+ }
72
+ catch (error) {
73
+ process.stderr.write('Error saving tokens:', error);
74
+ }
75
+ }
76
+ async readToken() {
77
+ try {
78
+ const token = (0, node_fs_1.readFileSync)(path.join(this.dir, 'auth.json'), 'utf8');
79
+ const decrypted = this.decipher.update(token, 'hex', 'utf8') + this.decipher.final('utf8');
80
+ return decrypted ? JSON.parse(decrypted) : null;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ async removeToken() {
87
+ const tokenPath = path.join(this.dir, 'auth.json');
88
+ if ((0, node_fs_1.existsSync)(tokenPath)) {
89
+ (0, node_fs_1.rmSync)(tokenPath);
90
+ }
91
+ }
92
+ }
93
+ exports.RedoclyOAuthClient = RedoclyOAuthClient;
@@ -0,0 +1,13 @@
1
+ import type { CommandArgs } from '../wrapper';
2
+ export declare function promptClientToken(domain: string): Promise<string>;
3
+ export type LoginOptions = {
4
+ verbose?: boolean;
5
+ residency?: string;
6
+ config?: string;
7
+ next?: boolean;
8
+ };
9
+ export declare function handleLogin({ argv, config, version }: CommandArgs<LoginOptions>): Promise<void>;
10
+ export type LogoutOptions = {
11
+ config?: string;
12
+ };
13
+ export declare function handleLogout({ version }: CommandArgs<LogoutOptions>): Promise<void>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptClientToken = promptClientToken;
4
+ exports.handleLogin = handleLogin;
5
+ exports.handleLogout = handleLogout;
6
+ const colorette_1 = require("colorette");
7
+ const openapi_core_1 = require("@redocly/openapi-core");
8
+ const miscellaneous_1 = require("../utils/miscellaneous");
9
+ const oauth_client_1 = require("../auth/oauth-client");
10
+ const api_1 = require("../reunite/api");
11
+ function promptClientToken(domain) {
12
+ return (0, miscellaneous_1.promptUser)((0, colorette_1.green)(`\n 🔑 Copy your API key from ${(0, colorette_1.blue)(`https://app.${domain}/profile`)} and paste it below`) + (0, colorette_1.yellow)(' (if you want to log in with Reunite, please run `redocly login --next` instead)'), true);
13
+ }
14
+ async function handleLogin({ argv, config, version }) {
15
+ if (argv.next) {
16
+ try {
17
+ const reuniteUrl = (0, api_1.getReuniteUrl)(argv.residency);
18
+ const oauthClient = new oauth_client_1.RedoclyOAuthClient('redocly-cli', version);
19
+ await oauthClient.login(reuniteUrl);
20
+ }
21
+ catch {
22
+ if (argv.residency) {
23
+ const reuniteUrl = (0, api_1.getReuniteUrl)(argv.residency);
24
+ (0, miscellaneous_1.exitWithError)(`❌ Connection to ${reuniteUrl} failed.`);
25
+ }
26
+ else {
27
+ (0, miscellaneous_1.exitWithError)(`❌ Login failed. Please check your credentials and try again.`);
28
+ }
29
+ }
30
+ }
31
+ else {
32
+ try {
33
+ const region = argv.residency || config.region;
34
+ const client = new openapi_core_1.RedoclyClient(region);
35
+ const clientToken = await promptClientToken(client.domain);
36
+ process.stdout.write((0, colorette_1.gray)('\n Logging in...\n'));
37
+ await client.login(clientToken, argv.verbose);
38
+ process.stdout.write((0, colorette_1.green)(' Authorization confirmed. ✅\n\n'));
39
+ }
40
+ catch (err) {
41
+ (0, miscellaneous_1.exitWithError)(' ' + err?.message);
42
+ }
43
+ }
44
+ }
45
+ async function handleLogout({ version }) {
46
+ const client = new openapi_core_1.RedoclyClient();
47
+ client.logout();
48
+ const oauthClient = new oauth_client_1.RedoclyOAuthClient('redocly-cli', version);
49
+ oauthClient.logout();
50
+ process.stdout.write('Logged out from the Redocly account. ✋ \n');
51
+ }
@@ -1,4 +1,4 @@
1
- import { handlePush as handleCMSPush } from '../cms/commands/push';
1
+ import { handlePush as handleCMSPush } from '../reunite/commands/push';
2
2
  import type { Config, Region } from '@redocly/openapi-core';
3
3
  import type { CommandArgs } from '../wrapper';
4
4
  import type { VerifyConfigOptions } from '../types';
@@ -13,9 +13,9 @@ const crypto_1 = require("crypto");
13
13
  const openapi_core_1 = require("@redocly/openapi-core");
14
14
  const utils_1 = require("@redocly/openapi-core/lib/utils");
15
15
  const miscellaneous_1 = require("../utils/miscellaneous");
16
- const login_1 = require("./login");
17
- const push_1 = require("../cms/commands/push");
18
- const api_client_1 = require("../cms/api/api-client");
16
+ const auth_1 = require("./auth");
17
+ const push_1 = require("../reunite/commands/push");
18
+ const api_client_1 = require("../reunite/api/api-client");
19
19
  const DEFAULT_VERSION = 'latest';
20
20
  exports.DESTINATION_REGEX =
21
21
  // eslint-disable-next-line no-useless-escape
@@ -31,7 +31,7 @@ async function handlePush({ argv, config }) {
31
31
  const isAuthorized = await client.isAuthorizedWithRedoclyByRegion();
32
32
  if (!isAuthorized) {
33
33
  try {
34
- const clientToken = await (0, login_1.promptClientToken)(client.domain);
34
+ const clientToken = await (0, auth_1.promptClientToken)(client.domain);
35
35
  await client.login(clientToken);
36
36
  }
37
37
  catch (e) {
@@ -111,9 +111,9 @@ function replace$Refs(obj, relativeFrom, componentFiles = {}) {
111
111
  const filesGroupName = componentFiles[groupName];
112
112
  if (!filesGroupName || !filesGroupName[name])
113
113
  return;
114
- let filename = path.relative(relativeFrom, filesGroupName[name].filename);
114
+ let filename = (0, openapi_core_1.slash)(path.relative(relativeFrom, filesGroupName[name].filename));
115
115
  if (!filename.startsWith('.')) {
116
- filename = '.' + path.sep + filename;
116
+ filename = './' + filename;
117
117
  }
118
118
  node[key] = filename;
119
119
  }
@@ -125,8 +125,8 @@ function implicitlyReferenceDiscriminator(obj, defName, filename, schemaFiles) {
125
125
  const implicitMapping = {};
126
126
  for (const [name, { inherits, filename: parentFilename }] of Object.entries(schemaFiles)) {
127
127
  if (inherits.indexOf(defPtr) > -1) {
128
- const res = path.relative(path.dirname(filename), parentFilename);
129
- implicitMapping[name] = res.startsWith('.') ? res : '.' + path.sep + res;
128
+ const res = (0, openapi_core_1.slash)(path.relative(path.dirname(filename), parentFilename));
129
+ implicitMapping[name] = res.startsWith('.') ? res : './' + res;
130
130
  }
131
131
  }
132
132
  if ((0, js_utils_1.isEmptyObject)(implicitMapping))
package/lib/index.js CHANGED
@@ -4,16 +4,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  require("./utils/assert-node-version");
5
5
  const yargs = require("yargs");
6
6
  const colors = require("colorette");
7
- const openapi_core_1 = require("@redocly/openapi-core");
8
7
  const types_1 = require("./types");
9
8
  const preview_docs_1 = require("./commands/preview-docs");
10
9
  const stats_1 = require("./commands/stats");
11
10
  const split_1 = require("./commands/split");
12
11
  const join_1 = require("./commands/join");
13
- const push_status_1 = require("./cms/commands/push-status");
12
+ const push_status_1 = require("./reunite/commands/push-status");
14
13
  const lint_1 = require("./commands/lint");
15
14
  const bundle_1 = require("./commands/bundle");
16
- const login_1 = require("./commands/login");
15
+ const auth_1 = require("./commands/auth");
17
16
  const build_docs_1 = require("./commands/build-docs");
18
17
  const update_version_notifier_1 = require("./utils/update-version-notifier");
19
18
  const wrapper_1 = require("./wrapper");
@@ -521,32 +520,32 @@ yargs
521
520
  process.env.REDOCLY_CLI_COMMAND = 'check-config';
522
521
  (0, wrapper_1.commandWrapper)()(argv);
523
522
  })
524
- .command('login', 'Login to the Redocly API registry with an access token.', async (yargs) => yargs.options({
523
+ .command('login', 'Log in to Redocly.', async (yargs) => yargs.options({
525
524
  verbose: {
526
525
  description: 'Include additional output.',
527
526
  type: 'boolean',
528
527
  },
529
- region: {
530
- description: 'Specify a region.',
531
- alias: 'r',
532
- choices: types_1.regionChoices,
528
+ residency: {
529
+ description: 'Residency of the application. Defaults to `us`.',
530
+ alias: ['r', 'region'],
531
+ type: 'string',
533
532
  },
534
533
  config: {
535
534
  description: 'Path to the config file.',
536
535
  requiresArg: true,
537
536
  type: 'string',
538
537
  },
538
+ next: {
539
+ description: 'Use Reunite application to login.',
540
+ type: 'boolean',
541
+ },
539
542
  }), (argv) => {
540
543
  process.env.REDOCLY_CLI_COMMAND = 'login';
541
- (0, wrapper_1.commandWrapper)(login_1.handleLogin)(argv);
544
+ (0, wrapper_1.commandWrapper)(auth_1.handleLogin)(argv);
542
545
  })
543
- .command('logout', 'Clear your stored credentials for the Redocly API registry.', (yargs) => yargs, async (argv) => {
546
+ .command('logout', 'Clear your stored credentials for the Redocly API registry.', (yargs) => yargs, (argv) => {
544
547
  process.env.REDOCLY_CLI_COMMAND = 'logout';
545
- await (0, wrapper_1.commandWrapper)(async () => {
546
- const client = new openapi_core_1.RedoclyClient();
547
- client.logout();
548
- process.stdout.write('Logged out from the Redocly account. ✋\n');
549
- })(argv);
548
+ (0, wrapper_1.commandWrapper)(auth_1.handleLogout)(argv);
550
549
  })
551
550
  .command('preview', 'Preview Redocly project using one of the product NPM packages.', (yargs) => yargs.options({
552
551
  product: {