@redocly/cli 1.29.0 → 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 (86) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/__tests__/commands/push-region.test.js +3 -3
  3. package/lib/auth/__tests__/device-flow.test.js +62 -0
  4. package/lib/auth/__tests__/oauth-client.test.js +93 -0
  5. package/lib/auth/device-flow.d.ts +26 -0
  6. package/lib/auth/device-flow.js +133 -0
  7. package/lib/auth/oauth-client.d.ts +14 -0
  8. package/lib/auth/oauth-client.js +93 -0
  9. package/lib/commands/auth.d.ts +13 -0
  10. package/lib/commands/auth.js +51 -0
  11. package/lib/commands/push.d.ts +1 -1
  12. package/lib/commands/push.js +4 -4
  13. package/lib/index.js +14 -15
  14. package/lib/otel.d.ts +10 -0
  15. package/lib/otel.js +47 -0
  16. package/lib/reunite/api/__tests__/domains.test.js +32 -0
  17. package/lib/{cms → reunite}/api/api-client.d.ts +9 -0
  18. package/lib/{cms → reunite}/api/api-client.js +2 -1
  19. package/lib/reunite/api/domains.d.ts +4 -0
  20. package/lib/reunite/api/domains.js +22 -0
  21. package/lib/reunite/commands/__tests__/push.test.d.ts +1 -0
  22. package/lib/reunite/commands/__tests__/utils.test.d.ts +1 -0
  23. package/lib/types.d.ts +4 -4
  24. package/lib/utils/miscellaneous.d.ts +5 -4
  25. package/lib/utils/miscellaneous.js +14 -14
  26. package/package.json +7 -2
  27. package/src/__tests__/commands/push-region.test.ts +2 -2
  28. package/src/auth/__tests__/device-flow.test.ts +73 -0
  29. package/src/auth/__tests__/oauth-client.test.ts +117 -0
  30. package/src/auth/device-flow.ts +175 -0
  31. package/src/auth/oauth-client.ts +111 -0
  32. package/src/commands/auth.ts +66 -0
  33. package/src/commands/push.ts +3 -3
  34. package/src/index.ts +14 -15
  35. package/src/otel.ts +59 -0
  36. package/src/reunite/api/__tests__/domains.test.ts +41 -0
  37. package/src/{cms → reunite}/api/api-client.ts +1 -1
  38. package/src/reunite/api/domains.ts +23 -0
  39. package/src/types.ts +4 -3
  40. package/src/utils/miscellaneous.ts +20 -18
  41. package/tsconfig.tsbuildinfo +1 -1
  42. package/lib/cms/api/__tests__/domains.test.js +0 -13
  43. package/lib/cms/api/domains.d.ts +0 -1
  44. package/lib/cms/api/domains.js +0 -11
  45. package/lib/commands/login.d.ts +0 -9
  46. package/lib/commands/login.js +0 -23
  47. package/src/cms/api/__tests__/domains.test.ts +0 -15
  48. package/src/cms/api/domains.ts +0 -11
  49. package/src/commands/login.ts +0 -34
  50. /package/lib/{cms/api/__tests__/api-keys.test.d.ts → auth/__tests__/device-flow.test.d.ts} +0 -0
  51. /package/lib/{cms/api/__tests__/api.client.test.d.ts → auth/__tests__/oauth-client.test.d.ts} +0 -0
  52. /package/lib/{cms/api/__tests__/domains.test.d.ts → reunite/api/__tests__/api-keys.test.d.ts} +0 -0
  53. /package/lib/{cms → reunite}/api/__tests__/api-keys.test.js +0 -0
  54. /package/lib/{cms/commands/__tests__/push-status.test.d.ts → reunite/api/__tests__/api.client.test.d.ts} +0 -0
  55. /package/lib/{cms → reunite}/api/__tests__/api.client.test.js +0 -0
  56. /package/lib/{cms/commands/__tests__/push.test.d.ts → reunite/api/__tests__/domains.test.d.ts} +0 -0
  57. /package/lib/{cms → reunite}/api/api-keys.d.ts +0 -0
  58. /package/lib/{cms → reunite}/api/api-keys.js +0 -0
  59. /package/lib/{cms → reunite}/api/index.d.ts +0 -0
  60. /package/lib/{cms → reunite}/api/index.js +0 -0
  61. /package/lib/{cms → reunite}/api/types.d.ts +0 -0
  62. /package/lib/{cms → reunite}/api/types.js +0 -0
  63. /package/lib/{cms/commands/__tests__/utils.test.d.ts → reunite/commands/__tests__/push-status.test.d.ts} +0 -0
  64. /package/lib/{cms → reunite}/commands/__tests__/push-status.test.js +0 -0
  65. /package/lib/{cms → reunite}/commands/__tests__/push.test.js +0 -0
  66. /package/lib/{cms → reunite}/commands/__tests__/utils.test.js +0 -0
  67. /package/lib/{cms → reunite}/commands/push-status.d.ts +0 -0
  68. /package/lib/{cms → reunite}/commands/push-status.js +0 -0
  69. /package/lib/{cms → reunite}/commands/push.d.ts +0 -0
  70. /package/lib/{cms → reunite}/commands/push.js +0 -0
  71. /package/lib/{cms → reunite}/commands/utils.d.ts +0 -0
  72. /package/lib/{cms → reunite}/commands/utils.js +0 -0
  73. /package/lib/{cms → reunite}/utils.d.ts +0 -0
  74. /package/lib/{cms → reunite}/utils.js +0 -0
  75. /package/src/{cms → reunite}/api/__tests__/api-keys.test.ts +0 -0
  76. /package/src/{cms → reunite}/api/__tests__/api.client.test.ts +0 -0
  77. /package/src/{cms → reunite}/api/api-keys.ts +0 -0
  78. /package/src/{cms → reunite}/api/index.ts +0 -0
  79. /package/src/{cms → reunite}/api/types.ts +0 -0
  80. /package/src/{cms → reunite}/commands/__tests__/push-status.test.ts +0 -0
  81. /package/src/{cms → reunite}/commands/__tests__/push.test.ts +0 -0
  82. /package/src/{cms → reunite}/commands/__tests__/utils.test.ts +0 -0
  83. /package/src/{cms → reunite}/commands/push-status.ts +0 -0
  84. /package/src/{cms → reunite}/commands/push.ts +0 -0
  85. /package/src/{cms → reunite}/commands/utils.ts +0 -0
  86. /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
+ }
@@ -19,9 +19,9 @@ import {
19
19
  getFallbackApisOrExit,
20
20
  dumpBundle,
21
21
  } from '../utils/miscellaneous';
22
- import { promptClientToken } from './login';
23
- import { handlePush as handleCMSPush } from '../cms/commands/push';
24
- import { streamToBuffer } from '../cms/api/api-client';
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';
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 './cms/commands/push-status';
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/login';
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 './cms/commands/push-status';
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
- 'Login to the Redocly API registry with an access token.',
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
- region: {
616
- description: 'Specify a region.',
617
- alias: 'r',
618
- choices: regionChoices,
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
- async (argv) => {
638
+ (argv) => {
636
639
  process.env.REDOCLY_CLI_COMMAND = 'logout';
637
- await commandWrapper(async () => {
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/login';
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 './cms/commands/push';
13
- import type { PushStatusOptions } from './cms/commands/push-status';
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