@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.
- package/CHANGELOG.md +23 -0
- package/lib/__tests__/commands/push-region.test.js +3 -3
- package/lib/__tests__/utils.test.js +1 -0
- package/lib/auth/__tests__/device-flow.test.js +62 -0
- package/lib/auth/__tests__/oauth-client.test.js +93 -0
- package/lib/auth/device-flow.d.ts +26 -0
- package/lib/auth/device-flow.js +133 -0
- package/lib/auth/oauth-client.d.ts +14 -0
- package/lib/auth/oauth-client.js +93 -0
- package/lib/commands/auth.d.ts +13 -0
- package/lib/commands/auth.js +51 -0
- package/lib/commands/push.d.ts +1 -1
- package/lib/commands/push.js +4 -4
- package/lib/commands/split/index.js +4 -4
- package/lib/index.js +14 -15
- package/lib/otel.d.ts +10 -0
- package/lib/otel.js +47 -0
- package/lib/reunite/api/__tests__/domains.test.js +32 -0
- package/lib/{cms → reunite}/api/api-client.d.ts +9 -0
- package/lib/{cms → reunite}/api/api-client.js +2 -1
- package/lib/reunite/api/domains.d.ts +4 -0
- package/lib/reunite/api/domains.js +22 -0
- package/lib/reunite/commands/__tests__/push.test.d.ts +1 -0
- package/lib/reunite/commands/__tests__/utils.test.d.ts +1 -0
- package/lib/types.d.ts +4 -4
- package/lib/utils/miscellaneous.d.ts +5 -4
- package/lib/utils/miscellaneous.js +14 -14
- package/package.json +7 -2
- package/src/__tests__/commands/push-region.test.ts +2 -2
- package/src/__tests__/utils.test.ts +1 -0
- package/src/auth/__tests__/device-flow.test.ts +73 -0
- package/src/auth/__tests__/oauth-client.test.ts +117 -0
- package/src/auth/device-flow.ts +175 -0
- package/src/auth/oauth-client.ts +111 -0
- package/src/commands/auth.ts +66 -0
- package/src/commands/push.ts +3 -3
- package/src/commands/split/index.ts +9 -9
- package/src/index.ts +14 -15
- package/src/otel.ts +59 -0
- package/src/reunite/api/__tests__/domains.test.ts +41 -0
- package/src/{cms → reunite}/api/api-client.ts +1 -1
- package/src/reunite/api/domains.ts +23 -0
- package/src/types.ts +4 -3
- package/src/utils/miscellaneous.ts +20 -18
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/cms/api/__tests__/domains.test.js +0 -13
- package/lib/cms/api/domains.d.ts +0 -1
- package/lib/cms/api/domains.js +0 -11
- package/lib/commands/login.d.ts +0 -9
- package/lib/commands/login.js +0 -23
- package/src/cms/api/__tests__/domains.test.ts +0 -15
- package/src/cms/api/domains.ts +0 -11
- package/src/commands/login.ts +0 -34
- /package/lib/{cms/api/__tests__/api-keys.test.d.ts → auth/__tests__/device-flow.test.d.ts} +0 -0
- /package/lib/{cms/api/__tests__/api.client.test.d.ts → auth/__tests__/oauth-client.test.d.ts} +0 -0
- /package/lib/{cms/api/__tests__/domains.test.d.ts → reunite/api/__tests__/api-keys.test.d.ts} +0 -0
- /package/lib/{cms → reunite}/api/__tests__/api-keys.test.js +0 -0
- /package/lib/{cms/commands/__tests__/push-status.test.d.ts → reunite/api/__tests__/api.client.test.d.ts} +0 -0
- /package/lib/{cms → reunite}/api/__tests__/api.client.test.js +0 -0
- /package/lib/{cms/commands/__tests__/push.test.d.ts → reunite/api/__tests__/domains.test.d.ts} +0 -0
- /package/lib/{cms → reunite}/api/api-keys.d.ts +0 -0
- /package/lib/{cms → reunite}/api/api-keys.js +0 -0
- /package/lib/{cms → reunite}/api/index.d.ts +0 -0
- /package/lib/{cms → reunite}/api/index.js +0 -0
- /package/lib/{cms → reunite}/api/types.d.ts +0 -0
- /package/lib/{cms → reunite}/api/types.js +0 -0
- /package/lib/{cms/commands/__tests__/utils.test.d.ts → reunite/commands/__tests__/push-status.test.d.ts} +0 -0
- /package/lib/{cms → reunite}/commands/__tests__/push-status.test.js +0 -0
- /package/lib/{cms → reunite}/commands/__tests__/push.test.js +0 -0
- /package/lib/{cms → reunite}/commands/__tests__/utils.test.js +0 -0
- /package/lib/{cms → reunite}/commands/push-status.d.ts +0 -0
- /package/lib/{cms → reunite}/commands/push-status.js +0 -0
- /package/lib/{cms → reunite}/commands/push.d.ts +0 -0
- /package/lib/{cms → reunite}/commands/push.js +0 -0
- /package/lib/{cms → reunite}/commands/utils.d.ts +0 -0
- /package/lib/{cms → reunite}/commands/utils.js +0 -0
- /package/lib/{cms → reunite}/utils.d.ts +0 -0
- /package/lib/{cms → reunite}/utils.js +0 -0
- /package/src/{cms → reunite}/api/__tests__/api-keys.test.ts +0 -0
- /package/src/{cms → reunite}/api/__tests__/api.client.test.ts +0 -0
- /package/src/{cms → reunite}/api/api-keys.ts +0 -0
- /package/src/{cms → reunite}/api/index.ts +0 -0
- /package/src/{cms → reunite}/api/types.ts +0 -0
- /package/src/{cms → reunite}/commands/__tests__/push-status.test.ts +0 -0
- /package/src/{cms → reunite}/commands/__tests__/push.test.ts +0 -0
- /package/src/{cms → reunite}/commands/__tests__/utils.test.ts +0 -0
- /package/src/{cms → reunite}/commands/push-status.ts +0 -0
- /package/src/{cms → reunite}/commands/push.ts +0 -0
- /package/src/{cms → reunite}/commands/utils.ts +0 -0
- /package/src/{cms → reunite}/utils.ts +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { blue, green } from 'colorette';
|
|
2
|
+
import * as childProcess from 'child_process';
|
|
3
|
+
import { ReuniteApiClient } from '../reunite/api/api-client';
|
|
4
|
+
|
|
5
|
+
export type AuthToken = {
|
|
6
|
+
access_token: string;
|
|
7
|
+
refresh_token?: string;
|
|
8
|
+
token_type?: string;
|
|
9
|
+
expires_in?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class RedoclyOAuthDeviceFlow {
|
|
13
|
+
private apiClient: ReuniteApiClient;
|
|
14
|
+
|
|
15
|
+
constructor(private baseUrl: string, private clientName: string, private version: string) {
|
|
16
|
+
this.apiClient = new ReuniteApiClient(this.version, 'login');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
const code = await this.getDeviceCode();
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
'Attempting to automatically open the SSO authorization page in your default browser.\n'
|
|
23
|
+
);
|
|
24
|
+
process.stdout.write(
|
|
25
|
+
'If the browser does not open or you wish to use a different device to authorize this request, open the following URL:\n\n'
|
|
26
|
+
);
|
|
27
|
+
process.stdout.write(blue(code.verificationUri));
|
|
28
|
+
process.stdout.write(`\n\n`);
|
|
29
|
+
process.stdout.write(`Then enter the code:\n\n`);
|
|
30
|
+
process.stdout.write(blue(code.userCode));
|
|
31
|
+
process.stdout.write(`\n\n`);
|
|
32
|
+
|
|
33
|
+
this.openBrowser(code.verificationUriComplete);
|
|
34
|
+
|
|
35
|
+
const accessToken = await this.pollingAccessToken(
|
|
36
|
+
code.deviceCode,
|
|
37
|
+
code.interval,
|
|
38
|
+
code.expiresIn
|
|
39
|
+
);
|
|
40
|
+
process.stdout.write(green('✅ Logged in\n\n'));
|
|
41
|
+
|
|
42
|
+
return accessToken;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private openBrowser(url: string) {
|
|
46
|
+
try {
|
|
47
|
+
const cmd =
|
|
48
|
+
process.platform === 'win32'
|
|
49
|
+
? `start ${url}`
|
|
50
|
+
: process.platform === 'darwin'
|
|
51
|
+
? `open ${url}`
|
|
52
|
+
: `xdg-open ${url}`;
|
|
53
|
+
|
|
54
|
+
childProcess.execSync(cmd);
|
|
55
|
+
} catch {
|
|
56
|
+
// silently fail if browser cannot be opened
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async verifyToken(accessToken: string) {
|
|
61
|
+
try {
|
|
62
|
+
const response = await this.sendRequest('/session', 'GET', undefined, {
|
|
63
|
+
Cookie: `accessToken=${accessToken};`,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return !!response.user;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async verifyApiKey(apiKey: string) {
|
|
73
|
+
try {
|
|
74
|
+
const response = await this.sendRequest('/api-keys-verify', 'POST', {
|
|
75
|
+
apiKey,
|
|
76
|
+
});
|
|
77
|
+
return !!response.success;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async refreshToken(refreshToken: string) {
|
|
84
|
+
const response = await this.sendRequest(`/device-rotate-token`, 'POST', {
|
|
85
|
+
grant_type: 'refresh_token',
|
|
86
|
+
client_name: this.clientName,
|
|
87
|
+
refresh_token: refreshToken,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.access_token) {
|
|
91
|
+
throw new Error('Failed to refresh token');
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
access_token: response.access_token,
|
|
95
|
+
refresh_token: response.refresh_token,
|
|
96
|
+
expires_in: response.expires_in,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async pollingAccessToken(
|
|
101
|
+
deviceCode: string,
|
|
102
|
+
interval: number,
|
|
103
|
+
expiresIn: number
|
|
104
|
+
): Promise<AuthToken> {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const intervalId = setInterval(async () => {
|
|
107
|
+
const response = await this.getAccessToken(deviceCode);
|
|
108
|
+
if (response.access_token) {
|
|
109
|
+
clearInterval(intervalId);
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
resolve(response);
|
|
112
|
+
}
|
|
113
|
+
if (response.error && response.error !== 'authorization_pending') {
|
|
114
|
+
clearInterval(intervalId);
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
reject(response.error_description);
|
|
117
|
+
}
|
|
118
|
+
}, interval * 1000);
|
|
119
|
+
|
|
120
|
+
const timeoutId = setTimeout(async () => {
|
|
121
|
+
clearInterval(intervalId);
|
|
122
|
+
clearTimeout(timeoutId);
|
|
123
|
+
reject('Authorization has expired. Please try again.');
|
|
124
|
+
}, expiresIn * 1000);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async getAccessToken(deviceCode: string) {
|
|
129
|
+
return await this.sendRequest('/device-token', 'POST', {
|
|
130
|
+
client_name: this.clientName,
|
|
131
|
+
device_code: deviceCode,
|
|
132
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async getDeviceCode() {
|
|
137
|
+
const {
|
|
138
|
+
device_code: deviceCode,
|
|
139
|
+
user_code: userCode,
|
|
140
|
+
verification_uri: verificationUri,
|
|
141
|
+
verification_uri_complete: verificationUriComplete,
|
|
142
|
+
interval = 10,
|
|
143
|
+
expires_in: expiresIn = 300,
|
|
144
|
+
} = await this.sendRequest('/device-authorize', 'POST', {
|
|
145
|
+
client_name: this.clientName,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
deviceCode,
|
|
150
|
+
userCode,
|
|
151
|
+
verificationUri,
|
|
152
|
+
verificationUriComplete,
|
|
153
|
+
interval,
|
|
154
|
+
expiresIn,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async sendRequest(
|
|
159
|
+
url: string,
|
|
160
|
+
method: string = 'GET',
|
|
161
|
+
body: Record<string, unknown> | undefined = undefined,
|
|
162
|
+
headers: Record<string, string> = {}
|
|
163
|
+
) {
|
|
164
|
+
url = `${this.baseUrl}${url}`;
|
|
165
|
+
const response = await this.apiClient.request(url, {
|
|
166
|
+
body: body ? JSON.stringify(body) : body,
|
|
167
|
+
method,
|
|
168
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
169
|
+
});
|
|
170
|
+
if (response.status === 204) {
|
|
171
|
+
return { success: true };
|
|
172
|
+
}
|
|
173
|
+
return await response.json();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { mkdirSync, existsSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
5
|
+
import { Buffer } from 'node:buffer';
|
|
6
|
+
import { type AuthToken, RedoclyOAuthDeviceFlow } from './device-flow';
|
|
7
|
+
|
|
8
|
+
const SALT = '4618dbc9-8aed-4e27-aaf0-225f4603e5a4';
|
|
9
|
+
const CRYPTO_ALGORITHM = 'aes-256-cbc';
|
|
10
|
+
|
|
11
|
+
export class RedoclyOAuthClient {
|
|
12
|
+
private dir: string;
|
|
13
|
+
private cipher: crypto.Cipher;
|
|
14
|
+
private decipher: crypto.Decipher;
|
|
15
|
+
|
|
16
|
+
constructor(private clientName: string, private version: string) {
|
|
17
|
+
this.dir = path.join(homedir(), '.redocly');
|
|
18
|
+
if (!existsSync(this.dir)) {
|
|
19
|
+
mkdirSync(this.dir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const homeDirPath = process.env.HOME as string;
|
|
23
|
+
const hash = crypto.createHash('sha256');
|
|
24
|
+
hash.update(`${homeDirPath}${SALT}`);
|
|
25
|
+
const hashHex = hash.digest('hex');
|
|
26
|
+
|
|
27
|
+
const key = Buffer.alloc(
|
|
28
|
+
32,
|
|
29
|
+
Buffer.from(hashHex).toString('base64')
|
|
30
|
+
).toString() as crypto.CipherKey;
|
|
31
|
+
const iv = Buffer.alloc(
|
|
32
|
+
16,
|
|
33
|
+
Buffer.from(process.env.HOME as string).toString('base64')
|
|
34
|
+
).toString() as crypto.BinaryLike;
|
|
35
|
+
this.cipher = crypto.createCipheriv(CRYPTO_ALGORITHM, key, iv);
|
|
36
|
+
this.decipher = crypto.createDecipheriv(CRYPTO_ALGORITHM, key, iv);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async login(baseUrl: string) {
|
|
40
|
+
const deviceFlow = new RedoclyOAuthDeviceFlow(baseUrl, this.clientName, this.version);
|
|
41
|
+
|
|
42
|
+
const token = await deviceFlow.run();
|
|
43
|
+
if (!token) {
|
|
44
|
+
throw new Error('Failed to login');
|
|
45
|
+
}
|
|
46
|
+
this.saveToken(token);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async logout() {
|
|
50
|
+
try {
|
|
51
|
+
this.removeToken();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// do nothing
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async isAuthorized(baseUrl: string, apiKey?: string) {
|
|
58
|
+
const deviceFlow = new RedoclyOAuthDeviceFlow(baseUrl, this.clientName, this.version);
|
|
59
|
+
|
|
60
|
+
if (apiKey) {
|
|
61
|
+
return await deviceFlow.verifyApiKey(apiKey);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const token = await this.readToken();
|
|
65
|
+
if (!token) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isValidAccessToken = await deviceFlow.verifyToken(token.access_token);
|
|
70
|
+
|
|
71
|
+
if (isValidAccessToken) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const newToken = await deviceFlow.refreshToken(token.refresh_token);
|
|
77
|
+
await this.saveToken(newToken);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async saveToken(token: AuthToken) {
|
|
86
|
+
try {
|
|
87
|
+
const encrypted =
|
|
88
|
+
this.cipher.update(JSON.stringify(token), 'utf8', 'hex') + this.cipher.final('hex');
|
|
89
|
+
writeFileSync(path.join(this.dir, 'auth.json'), encrypted);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
process.stderr.write('Error saving tokens:', error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async readToken() {
|
|
96
|
+
try {
|
|
97
|
+
const token = readFileSync(path.join(this.dir, 'auth.json'), 'utf8');
|
|
98
|
+
const decrypted = this.decipher.update(token, 'hex', 'utf8') + this.decipher.final('utf8');
|
|
99
|
+
return decrypted ? JSON.parse(decrypted) : null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async removeToken() {
|
|
106
|
+
const tokenPath = path.join(this.dir, 'auth.json');
|
|
107
|
+
if (existsSync(tokenPath)) {
|
|
108
|
+
rmSync(tokenPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { blue, green, gray, yellow } from 'colorette';
|
|
2
|
+
import { RedoclyClient } from '@redocly/openapi-core';
|
|
3
|
+
import { exitWithError, promptUser } from '../utils/miscellaneous';
|
|
4
|
+
import { RedoclyOAuthClient } from '../auth/oauth-client';
|
|
5
|
+
import { getReuniteUrl } from '../reunite/api';
|
|
6
|
+
|
|
7
|
+
import type { CommandArgs } from '../wrapper';
|
|
8
|
+
import type { Region } from '@redocly/openapi-core';
|
|
9
|
+
|
|
10
|
+
export function promptClientToken(domain: string) {
|
|
11
|
+
return promptUser(
|
|
12
|
+
green(
|
|
13
|
+
`\n 🔑 Copy your API key from ${blue(`https://app.${domain}/profile`)} and paste it below`
|
|
14
|
+
) + yellow(' (if you want to log in with Reunite, please run `redocly login --next` instead)'),
|
|
15
|
+
true
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type LoginOptions = {
|
|
20
|
+
verbose?: boolean;
|
|
21
|
+
residency?: string;
|
|
22
|
+
config?: string;
|
|
23
|
+
next?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export async function handleLogin({ argv, config, version }: CommandArgs<LoginOptions>) {
|
|
27
|
+
if (argv.next) {
|
|
28
|
+
try {
|
|
29
|
+
const reuniteUrl = getReuniteUrl(argv.residency);
|
|
30
|
+
const oauthClient = new RedoclyOAuthClient('redocly-cli', version);
|
|
31
|
+
await oauthClient.login(reuniteUrl);
|
|
32
|
+
} catch {
|
|
33
|
+
if (argv.residency) {
|
|
34
|
+
const reuniteUrl = getReuniteUrl(argv.residency);
|
|
35
|
+
exitWithError(`❌ Connection to ${reuniteUrl} failed.`);
|
|
36
|
+
} else {
|
|
37
|
+
exitWithError(`❌ Login failed. Please check your credentials and try again.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
const region = (argv.residency as Region) || config.region;
|
|
43
|
+
const client = new RedoclyClient(region);
|
|
44
|
+
const clientToken = await promptClientToken(client.domain);
|
|
45
|
+
process.stdout.write(gray('\n Logging in...\n'));
|
|
46
|
+
await client.login(clientToken, argv.verbose);
|
|
47
|
+
process.stdout.write(green(' Authorization confirmed. ✅\n\n'));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
exitWithError(' ' + err?.message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type LogoutOptions = {
|
|
55
|
+
config?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export async function handleLogout({ version }: CommandArgs<LogoutOptions>) {
|
|
59
|
+
const client = new RedoclyClient();
|
|
60
|
+
client.logout();
|
|
61
|
+
|
|
62
|
+
const oauthClient = new RedoclyOAuthClient('redocly-cli', version);
|
|
63
|
+
oauthClient.logout();
|
|
64
|
+
|
|
65
|
+
process.stdout.write('Logged out from the Redocly account. ✋ \n');
|
|
66
|
+
}
|
package/src/commands/push.ts
CHANGED
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
getFallbackApisOrExit,
|
|
20
20
|
dumpBundle,
|
|
21
21
|
} from '../utils/miscellaneous';
|
|
22
|
-
import { promptClientToken } from './
|
|
23
|
-
import { handlePush as handleCMSPush } from '../
|
|
24
|
-
import { streamToBuffer } from '../
|
|
22
|
+
import { promptClientToken } from './auth';
|
|
23
|
+
import { handlePush as handleCMSPush } from '../reunite/commands/push';
|
|
24
|
+
import { streamToBuffer } from '../reunite/api/api-client';
|
|
25
25
|
|
|
26
26
|
import type { Readable } from 'node:stream';
|
|
27
27
|
import type { Agent } from 'node:http';
|
|
@@ -46,16 +46,16 @@ export type SplitOptions = {
|
|
|
46
46
|
export async function handleSplit({ argv, collectSpecData }: CommandArgs<SplitOptions>) {
|
|
47
47
|
const startedAt = performance.now();
|
|
48
48
|
const { api, outDir, separator } = argv;
|
|
49
|
-
validateDefinitionFileName(api
|
|
49
|
+
validateDefinitionFileName(api);
|
|
50
50
|
const ext = getAndValidateFileExtension(api);
|
|
51
|
-
const openapi = readYaml(api
|
|
51
|
+
const openapi = readYaml(api) as Oas3Definition | Oas3_1Definition;
|
|
52
52
|
collectSpecData?.(openapi);
|
|
53
53
|
splitDefinition(openapi, outDir, separator, ext);
|
|
54
54
|
process.stderr.write(
|
|
55
|
-
`🪓 Document: ${blue(api
|
|
55
|
+
`🪓 Document: ${blue(api)} ${green('is successfully split')}
|
|
56
56
|
and all related files are saved to the directory: ${blue(outDir)} \n`
|
|
57
57
|
);
|
|
58
|
-
printExecutionTime('split', startedAt, api
|
|
58
|
+
printExecutionTime('split', startedAt, api);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function splitDefinition(
|
|
@@ -176,9 +176,9 @@ function replace$Refs(obj: unknown, relativeFrom: string, componentFiles = {} as
|
|
|
176
176
|
const groupName = splittedNode[2];
|
|
177
177
|
const filesGroupName = componentFiles[groupName];
|
|
178
178
|
if (!filesGroupName || !filesGroupName[name!]) return;
|
|
179
|
-
let filename = path.relative(relativeFrom, filesGroupName[name!].filename);
|
|
179
|
+
let filename = slash(path.relative(relativeFrom, filesGroupName[name!].filename));
|
|
180
180
|
if (!filename.startsWith('.')) {
|
|
181
|
-
filename = '
|
|
181
|
+
filename = './' + filename;
|
|
182
182
|
}
|
|
183
183
|
node[key] = filename;
|
|
184
184
|
}
|
|
@@ -192,11 +192,11 @@ function implicitlyReferenceDiscriminator(
|
|
|
192
192
|
) {
|
|
193
193
|
if (!obj.discriminator) return;
|
|
194
194
|
const defPtr = `#/${COMPONENTS}/${OPENAPI3_COMPONENT.Schemas}/${defName}`;
|
|
195
|
-
const implicitMapping = {}
|
|
195
|
+
const implicitMapping: Record<string, string> = {};
|
|
196
196
|
for (const [name, { inherits, filename: parentFilename }] of Object.entries(schemaFiles) as any) {
|
|
197
197
|
if (inherits.indexOf(defPtr) > -1) {
|
|
198
|
-
const res = path.relative(path.dirname(filename), parentFilename);
|
|
199
|
-
implicitMapping[name] = res.startsWith('.') ? res : '
|
|
198
|
+
const res = slash(path.relative(path.dirname(filename), parentFilename));
|
|
199
|
+
implicitMapping[name] = res.startsWith('.') ? res : './' + res;
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
package/src/index.ts
CHANGED
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
import './utils/assert-node-version';
|
|
4
4
|
import * as yargs from 'yargs';
|
|
5
5
|
import * as colors from 'colorette';
|
|
6
|
-
import { RedoclyClient } from '@redocly/openapi-core';
|
|
7
6
|
import { outputExtensions, regionChoices } from './types';
|
|
8
7
|
import { previewDocs } from './commands/preview-docs';
|
|
9
8
|
import { handleStats } from './commands/stats';
|
|
10
9
|
import { handleSplit } from './commands/split';
|
|
11
10
|
import { handleJoin } from './commands/join';
|
|
12
|
-
import { handlePushStatus } from './
|
|
11
|
+
import { handlePushStatus } from './reunite/commands/push-status';
|
|
13
12
|
import { handleLint } from './commands/lint';
|
|
14
13
|
import { handleBundle } from './commands/bundle';
|
|
15
|
-
import { handleLogin } from './commands/
|
|
14
|
+
import { handleLogin, handleLogout } from './commands/auth';
|
|
16
15
|
import { handlerBuildCommand } from './commands/build-docs';
|
|
17
16
|
import {
|
|
18
17
|
cacheLatestVersion,
|
|
@@ -29,7 +28,7 @@ import { commonPushHandler } from './commands/push';
|
|
|
29
28
|
import type { Arguments } from 'yargs';
|
|
30
29
|
import type { OutputFormat, RuleSeverity } from '@redocly/openapi-core';
|
|
31
30
|
import type { BuildDocsArgv } from './commands/build-docs/types';
|
|
32
|
-
import type { PushStatusOptions } from './
|
|
31
|
+
import type { PushStatusOptions } from './reunite/commands/push-status';
|
|
33
32
|
import type { PushArguments } from './types';
|
|
34
33
|
import type { EjectOptions } from './commands/eject';
|
|
35
34
|
|
|
@@ -605,23 +604,27 @@ yargs
|
|
|
605
604
|
)
|
|
606
605
|
.command(
|
|
607
606
|
'login',
|
|
608
|
-
'
|
|
607
|
+
'Log in to Redocly.',
|
|
609
608
|
async (yargs) =>
|
|
610
609
|
yargs.options({
|
|
611
610
|
verbose: {
|
|
612
611
|
description: 'Include additional output.',
|
|
613
612
|
type: 'boolean',
|
|
614
613
|
},
|
|
615
|
-
|
|
616
|
-
description: '
|
|
617
|
-
alias: 'r',
|
|
618
|
-
|
|
614
|
+
residency: {
|
|
615
|
+
description: 'Residency of the application. Defaults to `us`.',
|
|
616
|
+
alias: ['r', 'region'],
|
|
617
|
+
type: 'string',
|
|
619
618
|
},
|
|
620
619
|
config: {
|
|
621
620
|
description: 'Path to the config file.',
|
|
622
621
|
requiresArg: true,
|
|
623
622
|
type: 'string',
|
|
624
623
|
},
|
|
624
|
+
next: {
|
|
625
|
+
description: 'Use Reunite application to login.',
|
|
626
|
+
type: 'boolean',
|
|
627
|
+
},
|
|
625
628
|
}),
|
|
626
629
|
(argv) => {
|
|
627
630
|
process.env.REDOCLY_CLI_COMMAND = 'login';
|
|
@@ -632,13 +635,9 @@ yargs
|
|
|
632
635
|
'logout',
|
|
633
636
|
'Clear your stored credentials for the Redocly API registry.',
|
|
634
637
|
(yargs) => yargs,
|
|
635
|
-
|
|
638
|
+
(argv) => {
|
|
636
639
|
process.env.REDOCLY_CLI_COMMAND = 'logout';
|
|
637
|
-
|
|
638
|
-
const client = new RedoclyClient();
|
|
639
|
-
client.logout();
|
|
640
|
-
process.stdout.write('Logged out from the Redocly account. ✋\n');
|
|
641
|
-
})(argv);
|
|
640
|
+
commandWrapper(handleLogout)(argv);
|
|
642
641
|
}
|
|
643
642
|
)
|
|
644
643
|
.command(
|
package/src/otel.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { trace } from '@opentelemetry/api';
|
|
2
|
+
import { Resource as OtelResource } from '@opentelemetry/resources';
|
|
3
|
+
import { NodeTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
|
|
4
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
5
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
6
|
+
import { version } from './utils/update-version-notifier';
|
|
7
|
+
import { DEFAULT_FETCH_TIMEOUT } from './utils/fetch-with-timeout';
|
|
8
|
+
|
|
9
|
+
import type { Analytics } from './utils/miscellaneous';
|
|
10
|
+
|
|
11
|
+
type Events = {
|
|
12
|
+
[key: string]: Analytics;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const OTEL_TRACES_URL = process.env.OTEL_TRACES_URL || 'https://otel.cloud.redocly.com/v1/traces';
|
|
16
|
+
|
|
17
|
+
export class OtelServerTelemetry {
|
|
18
|
+
init() {
|
|
19
|
+
const nodeTracerProvider = new NodeTracerProvider({
|
|
20
|
+
resource: new OtelResource({
|
|
21
|
+
[ATTR_SERVICE_NAME]: `redocly-cli`,
|
|
22
|
+
[ATTR_SERVICE_VERSION]: `@redocly/cli@${version}`,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
nodeTracerProvider.addSpanProcessor(
|
|
27
|
+
new SimpleSpanProcessor(
|
|
28
|
+
new OTLPTraceExporter({
|
|
29
|
+
url: OTEL_TRACES_URL,
|
|
30
|
+
headers: {},
|
|
31
|
+
timeoutMillis: DEFAULT_FETCH_TIMEOUT,
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
nodeTracerProvider.register();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
send<K extends keyof Events>(event: K, data: Events[K]): void {
|
|
40
|
+
const time = new Date();
|
|
41
|
+
const eventId = crypto.randomUUID();
|
|
42
|
+
const span = trace.getTracer('CliTelemetry').startSpan(`event.${event}`, {
|
|
43
|
+
attributes: {
|
|
44
|
+
'cloudevents.event_client.id': eventId,
|
|
45
|
+
'cloudevents.event_client.type': event,
|
|
46
|
+
},
|
|
47
|
+
startTime: time,
|
|
48
|
+
});
|
|
49
|
+
for (const [key, value] of Object.entries(data)) {
|
|
50
|
+
const keySnakeCase = key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
span.setAttribute(`cloudevents.event_data.${keySnakeCase}`, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
span.end(time);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const otelTelemetry = new OtelServerTelemetry();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getDomain } from '../domains';
|
|
2
|
+
import { getReuniteUrl } from '../domains';
|
|
3
|
+
|
|
4
|
+
import type { Region } from '@redocly/openapi-core';
|
|
5
|
+
|
|
6
|
+
describe('getDomain()', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
delete process.env.REDOCLY_DOMAIN;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return the domain from environment variable', () => {
|
|
12
|
+
process.env.REDOCLY_DOMAIN = 'test-domain';
|
|
13
|
+
|
|
14
|
+
expect(getDomain()).toBe('test-domain');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return the default domain if no domain provided', () => {
|
|
18
|
+
process.env.REDOCLY_DOMAIN = '';
|
|
19
|
+
|
|
20
|
+
expect(getDomain()).toBe('https://app.cloud.redocly.com');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('getReuniteUrl()', () => {
|
|
25
|
+
it('should return US API URL when US region specified', () => {
|
|
26
|
+
expect(getReuniteUrl('us')).toBe('https://app.cloud.redocly.com/api');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return EU API URL when EU region specified', () => {
|
|
30
|
+
expect(getReuniteUrl('eu')).toBe('https://app.cloud.eu.redocly.com/api');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return custom domain API URL when custom domain specified', () => {
|
|
34
|
+
const customDomain = 'https://custom.domain.com';
|
|
35
|
+
expect(getReuniteUrl(customDomain as Region)).toBe('https://custom.domain.com/api');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return US API URL when no region specified', () => {
|
|
39
|
+
expect(getReuniteUrl()).toBe('https://app.cloud.redocly.com/api');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -26,7 +26,7 @@ export class ReuniteApiError extends Error {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
class ReuniteApiClient implements BaseApiClient {
|
|
29
|
+
export class ReuniteApiClient implements BaseApiClient {
|
|
30
30
|
public sunsetWarnings: SunsetWarningsBuffer = [];
|
|
31
31
|
|
|
32
32
|
constructor(protected version: string, protected command: string) {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Region } from '@redocly/openapi-core';
|
|
2
|
+
|
|
3
|
+
export const REUNITE_URLS: Record<Region, string> = {
|
|
4
|
+
us: 'https://app.cloud.redocly.com',
|
|
5
|
+
eu: 'https://app.cloud.eu.redocly.com',
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export function getDomain(): string {
|
|
9
|
+
return process.env.REDOCLY_DOMAIN || REUNITE_URLS.us;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getReuniteUrl(residency?: string) {
|
|
13
|
+
if (!residency) residency = 'us';
|
|
14
|
+
|
|
15
|
+
let reuniteUrl: string = REUNITE_URLS[residency as Region];
|
|
16
|
+
|
|
17
|
+
if (!reuniteUrl) {
|
|
18
|
+
reuniteUrl = residency;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const url = new URL('/api', reuniteUrl).toString();
|
|
22
|
+
return url;
|
|
23
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -3,14 +3,14 @@ import type { ArgumentsCamelCase } from 'yargs';
|
|
|
3
3
|
import type { LintOptions } from './commands/lint';
|
|
4
4
|
import type { BundleOptions } from './commands/bundle';
|
|
5
5
|
import type { JoinOptions } from './commands/join';
|
|
6
|
-
import type { LoginOptions } from './commands/
|
|
6
|
+
import type { LoginOptions, LogoutOptions } from './commands/auth';
|
|
7
7
|
import type { PushOptions } from './commands/push';
|
|
8
8
|
import type { StatsOptions } from './commands/stats';
|
|
9
9
|
import type { SplitOptions } from './commands/split';
|
|
10
10
|
import type { PreviewDocsOptions } from './commands/preview-docs';
|
|
11
11
|
import type { BuildDocsArgv } from './commands/build-docs/types';
|
|
12
|
-
import type { PushOptions as CMSPushOptions } from './
|
|
13
|
-
import type { PushStatusOptions } from './
|
|
12
|
+
import type { PushOptions as CMSPushOptions } from './reunite/commands/push';
|
|
13
|
+
import type { PushStatusOptions } from './reunite/commands/push-status';
|
|
14
14
|
import type { PreviewProjectOptions } from './commands/preview-project/types';
|
|
15
15
|
import type { TranslationsOptions } from './commands/translations';
|
|
16
16
|
import type { EjectOptions } from './commands/eject';
|
|
@@ -37,6 +37,7 @@ export type CommandOptions =
|
|
|
37
37
|
| LintOptions
|
|
38
38
|
| BundleOptions
|
|
39
39
|
| LoginOptions
|
|
40
|
+
| LogoutOptions
|
|
40
41
|
| PreviewDocsOptions
|
|
41
42
|
| BuildDocsArgv
|
|
42
43
|
| PushStatusOptions
|