@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
package/lib/otel.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Analytics } from './utils/miscellaneous';
|
|
2
|
+
type Events = {
|
|
3
|
+
[key: string]: Analytics;
|
|
4
|
+
};
|
|
5
|
+
export declare class OtelServerTelemetry {
|
|
6
|
+
init(): void;
|
|
7
|
+
send<K extends keyof Events>(event: K, data: Events[K]): void;
|
|
8
|
+
}
|
|
9
|
+
export declare const otelTelemetry: OtelServerTelemetry;
|
|
10
|
+
export {};
|
package/lib/otel.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.otelTelemetry = exports.OtelServerTelemetry = void 0;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
5
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
6
|
+
const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
|
|
7
|
+
const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
|
|
8
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
9
|
+
const update_version_notifier_1 = require("./utils/update-version-notifier");
|
|
10
|
+
const fetch_with_timeout_1 = require("./utils/fetch-with-timeout");
|
|
11
|
+
const OTEL_TRACES_URL = process.env.OTEL_TRACES_URL || 'https://otel.cloud.redocly.com/v1/traces';
|
|
12
|
+
class OtelServerTelemetry {
|
|
13
|
+
init() {
|
|
14
|
+
const nodeTracerProvider = new sdk_trace_node_1.NodeTracerProvider({
|
|
15
|
+
resource: new resources_1.Resource({
|
|
16
|
+
[semantic_conventions_1.ATTR_SERVICE_NAME]: `redocly-cli`,
|
|
17
|
+
[semantic_conventions_1.ATTR_SERVICE_VERSION]: `@redocly/cli@${update_version_notifier_1.version}`,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
nodeTracerProvider.addSpanProcessor(new sdk_trace_node_1.SimpleSpanProcessor(new exporter_trace_otlp_http_1.OTLPTraceExporter({
|
|
21
|
+
url: OTEL_TRACES_URL,
|
|
22
|
+
headers: {},
|
|
23
|
+
timeoutMillis: fetch_with_timeout_1.DEFAULT_FETCH_TIMEOUT,
|
|
24
|
+
})));
|
|
25
|
+
nodeTracerProvider.register();
|
|
26
|
+
}
|
|
27
|
+
send(event, data) {
|
|
28
|
+
const time = new Date();
|
|
29
|
+
const eventId = crypto.randomUUID();
|
|
30
|
+
const span = api_1.trace.getTracer('CliTelemetry').startSpan(`event.${event}`, {
|
|
31
|
+
attributes: {
|
|
32
|
+
'cloudevents.event_client.id': eventId,
|
|
33
|
+
'cloudevents.event_client.type': event,
|
|
34
|
+
},
|
|
35
|
+
startTime: time,
|
|
36
|
+
});
|
|
37
|
+
for (const [key, value] of Object.entries(data)) {
|
|
38
|
+
const keySnakeCase = key.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
39
|
+
if (value !== undefined) {
|
|
40
|
+
span.setAttribute(`cloudevents.event_data.${keySnakeCase}`, value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
span.end(time);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.OtelServerTelemetry = OtelServerTelemetry;
|
|
47
|
+
exports.otelTelemetry = new OtelServerTelemetry();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const domains_1 = require("../domains");
|
|
4
|
+
const domains_2 = require("../domains");
|
|
5
|
+
describe('getDomain()', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
delete process.env.REDOCLY_DOMAIN;
|
|
8
|
+
});
|
|
9
|
+
it('should return the domain from environment variable', () => {
|
|
10
|
+
process.env.REDOCLY_DOMAIN = 'test-domain';
|
|
11
|
+
expect((0, domains_1.getDomain)()).toBe('test-domain');
|
|
12
|
+
});
|
|
13
|
+
it('should return the default domain if no domain provided', () => {
|
|
14
|
+
process.env.REDOCLY_DOMAIN = '';
|
|
15
|
+
expect((0, domains_1.getDomain)()).toBe('https://app.cloud.redocly.com');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('getReuniteUrl()', () => {
|
|
19
|
+
it('should return US API URL when US region specified', () => {
|
|
20
|
+
expect((0, domains_2.getReuniteUrl)('us')).toBe('https://app.cloud.redocly.com/api');
|
|
21
|
+
});
|
|
22
|
+
it('should return EU API URL when EU region specified', () => {
|
|
23
|
+
expect((0, domains_2.getReuniteUrl)('eu')).toBe('https://app.cloud.eu.redocly.com/api');
|
|
24
|
+
});
|
|
25
|
+
it('should return custom domain API URL when custom domain specified', () => {
|
|
26
|
+
const customDomain = 'https://custom.domain.com';
|
|
27
|
+
expect((0, domains_2.getReuniteUrl)(customDomain)).toBe('https://custom.domain.com/api');
|
|
28
|
+
});
|
|
29
|
+
it('should return US API URL when no region specified', () => {
|
|
30
|
+
expect((0, domains_2.getReuniteUrl)()).toBe('https://app.cloud.redocly.com/api');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -15,6 +15,15 @@ export declare class ReuniteApiError extends Error {
|
|
|
15
15
|
status: number;
|
|
16
16
|
constructor(message: string, status: number);
|
|
17
17
|
}
|
|
18
|
+
export declare class ReuniteApiClient implements BaseApiClient {
|
|
19
|
+
protected version: string;
|
|
20
|
+
protected command: string;
|
|
21
|
+
sunsetWarnings: SunsetWarningsBuffer;
|
|
22
|
+
constructor(version: string, command: string);
|
|
23
|
+
request(url: string, options: FetchWithTimeoutOptions): Promise<Response>;
|
|
24
|
+
private collectSunsetWarning;
|
|
25
|
+
private getSunsetDate;
|
|
26
|
+
}
|
|
18
27
|
declare class RemotesApi {
|
|
19
28
|
private client;
|
|
20
29
|
private readonly domain;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ReuniteApi = exports.ReuniteApiError = void 0;
|
|
3
|
+
exports.ReuniteApi = exports.ReuniteApiClient = exports.ReuniteApiError = void 0;
|
|
4
4
|
exports.streamToBuffer = streamToBuffer;
|
|
5
5
|
const colorette_1 = require("colorette");
|
|
6
6
|
const fetch_with_timeout_1 = require("../../utils/fetch-with-timeout");
|
|
@@ -59,6 +59,7 @@ class ReuniteApiClient {
|
|
|
59
59
|
return Date.parse(sunsetDate);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
exports.ReuniteApiClient = ReuniteApiClient;
|
|
62
63
|
class RemotesApi {
|
|
63
64
|
constructor(client, domain, apiKey) {
|
|
64
65
|
this.client = client;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REUNITE_URLS = void 0;
|
|
4
|
+
exports.getDomain = getDomain;
|
|
5
|
+
exports.getReuniteUrl = getReuniteUrl;
|
|
6
|
+
exports.REUNITE_URLS = {
|
|
7
|
+
us: 'https://app.cloud.redocly.com',
|
|
8
|
+
eu: 'https://app.cloud.eu.redocly.com',
|
|
9
|
+
};
|
|
10
|
+
function getDomain() {
|
|
11
|
+
return process.env.REDOCLY_DOMAIN || exports.REUNITE_URLS.us;
|
|
12
|
+
}
|
|
13
|
+
function getReuniteUrl(residency) {
|
|
14
|
+
if (!residency)
|
|
15
|
+
residency = 'us';
|
|
16
|
+
let reuniteUrl = exports.REUNITE_URLS[residency];
|
|
17
|
+
if (!reuniteUrl) {
|
|
18
|
+
reuniteUrl = residency;
|
|
19
|
+
}
|
|
20
|
+
const url = new URL('/api', reuniteUrl).toString();
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/types.d.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';
|
|
@@ -27,7 +27,7 @@ export type Entrypoint = {
|
|
|
27
27
|
export declare const outputExtensions: ReadonlyArray<BundleOutputFormat>;
|
|
28
28
|
export type OutputExtensions = 'json' | 'yaml' | 'yml' | undefined;
|
|
29
29
|
export declare const regionChoices: ReadonlyArray<Region>;
|
|
30
|
-
export type CommandOptions = StatsOptions | SplitOptions | JoinOptions | PushOptions | CMSPushOptions | LintOptions | BundleOptions | LoginOptions | PreviewDocsOptions | BuildDocsArgv | PushStatusOptions | PreviewProjectOptions | TranslationsOptions | EjectOptions;
|
|
30
|
+
export type CommandOptions = StatsOptions | SplitOptions | JoinOptions | PushOptions | CMSPushOptions | LintOptions | BundleOptions | LoginOptions | LogoutOptions | PreviewDocsOptions | BuildDocsArgv | PushStatusOptions | PreviewProjectOptions | TranslationsOptions | EjectOptions;
|
|
31
31
|
export type VerifyConfigOptions = {
|
|
32
32
|
config?: string;
|
|
33
33
|
'lint-config'?: RuleSeverity;
|
|
@@ -59,17 +59,18 @@ export type ExitCode = 0 | 1 | 2;
|
|
|
59
59
|
export type Analytics = {
|
|
60
60
|
event: string;
|
|
61
61
|
event_time: string;
|
|
62
|
-
logged_in:
|
|
63
|
-
command: string
|
|
64
|
-
arguments:
|
|
62
|
+
logged_in: 'yes' | 'no';
|
|
63
|
+
command: string;
|
|
64
|
+
arguments: string;
|
|
65
65
|
node_version: string;
|
|
66
66
|
npm_version: string;
|
|
67
|
+
os_platform: string;
|
|
67
68
|
version: string;
|
|
68
69
|
exit_code: ExitCode;
|
|
69
70
|
environment?: string;
|
|
70
71
|
environment_ci?: string;
|
|
71
72
|
raw_input: string;
|
|
72
|
-
has_config?:
|
|
73
|
+
has_config?: 'yes' | 'no';
|
|
73
74
|
spec_version?: string;
|
|
74
75
|
spec_keyword?: string;
|
|
75
76
|
spec_full_version?: string;
|
|
@@ -37,6 +37,7 @@ const colorette_1 = require("colorette");
|
|
|
37
37
|
const perf_hooks_1 = require("perf_hooks");
|
|
38
38
|
const glob = require("glob");
|
|
39
39
|
const fs = require("fs");
|
|
40
|
+
const os = require("os");
|
|
40
41
|
const readline = require("readline");
|
|
41
42
|
const stream_1 = require("stream");
|
|
42
43
|
const child_process_1 = require("child_process");
|
|
@@ -48,7 +49,9 @@ const reference_docs_config_schema_1 = require("@redocly/config/lib/reference-do
|
|
|
48
49
|
const types_1 = require("../types");
|
|
49
50
|
const update_version_notifier_1 = require("./update-version-notifier");
|
|
50
51
|
const push_1 = require("../commands/push");
|
|
51
|
-
const
|
|
52
|
+
const oauth_client_1 = require("../auth/oauth-client");
|
|
53
|
+
const api_1 = require("../reunite/api");
|
|
54
|
+
const otel_1 = require("../otel");
|
|
52
55
|
async function getFallbackApisOrExit(argsApis, config) {
|
|
53
56
|
const { apis } = config;
|
|
54
57
|
const shouldFallbackToAllDefinitions = !(0, utils_1.isNotEmptyArray)(argsApis) && (0, utils_1.isNotEmptyObject)(apis);
|
|
@@ -440,6 +443,7 @@ function cleanColors(input) {
|
|
|
440
443
|
// eslint-disable-next-line no-control-regex
|
|
441
444
|
return input.replace(/\x1b\[\d+m/g, '');
|
|
442
445
|
}
|
|
446
|
+
otel_1.otelTelemetry.init();
|
|
443
447
|
async function sendTelemetry(argv, exit_code, has_config, spec_version, spec_keyword, spec_full_version) {
|
|
444
448
|
try {
|
|
445
449
|
if (!argv) {
|
|
@@ -448,33 +452,29 @@ async function sendTelemetry(argv, exit_code, has_config, spec_version, spec_key
|
|
|
448
452
|
const { _: [command], $0: _, ...args } = argv;
|
|
449
453
|
const event_time = new Date().toISOString();
|
|
450
454
|
const redoclyClient = new openapi_core_1.RedoclyClient();
|
|
451
|
-
const
|
|
455
|
+
const oauthClient = new oauth_client_1.RedoclyOAuthClient('redocly-cli', update_version_notifier_1.version);
|
|
456
|
+
const reuniteUrl = (0, api_1.getReuniteUrl)(argv.residency);
|
|
457
|
+
const logged_in = redoclyClient.hasTokens() || (await oauthClient.isAuthorized(reuniteUrl));
|
|
452
458
|
const data = {
|
|
453
459
|
event: 'cli_command',
|
|
454
460
|
event_time,
|
|
455
|
-
logged_in,
|
|
456
|
-
command
|
|
457
|
-
arguments: cleanArgs(args),
|
|
461
|
+
logged_in: logged_in ? 'yes' : 'no',
|
|
462
|
+
command: `${command}`,
|
|
463
|
+
arguments: JSON.stringify(cleanArgs(args)),
|
|
458
464
|
node_version: process.version,
|
|
459
465
|
npm_version: (0, child_process_1.execSync)('npm -v').toString().replace('\n', ''),
|
|
466
|
+
os_platform: os.platform(),
|
|
460
467
|
version: update_version_notifier_1.version,
|
|
461
468
|
exit_code,
|
|
462
469
|
environment: process.env.REDOCLY_ENVIRONMENT,
|
|
463
470
|
environment_ci: process.env.CI,
|
|
464
471
|
raw_input: cleanRawInput(process.argv.slice(2)),
|
|
465
|
-
has_config,
|
|
472
|
+
has_config: has_config ? 'yes' : 'no',
|
|
466
473
|
spec_version,
|
|
467
474
|
spec_keyword,
|
|
468
475
|
spec_full_version,
|
|
469
476
|
};
|
|
470
|
-
|
|
471
|
-
timeout: fetch_with_timeout_1.DEFAULT_FETCH_TIMEOUT,
|
|
472
|
-
method: 'POST',
|
|
473
|
-
headers: {
|
|
474
|
-
'content-type': 'application/json',
|
|
475
|
-
},
|
|
476
|
-
body: JSON.stringify(data),
|
|
477
|
-
});
|
|
477
|
+
otel_1.otelTelemetry.send(data.command, data);
|
|
478
478
|
}
|
|
479
479
|
catch (err) {
|
|
480
480
|
// Do nothing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"Roman Hotsiy <roman@redocly.com> (https://redocly.com/)"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@redocly/openapi-core": "1.
|
|
39
|
+
"@redocly/openapi-core": "1.30.0",
|
|
40
40
|
"abort-controller": "^3.0.0",
|
|
41
41
|
"chokidar": "^3.5.1",
|
|
42
42
|
"colorette": "^1.2.0",
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
"glob": "^7.1.6",
|
|
47
47
|
"handlebars": "^4.7.6",
|
|
48
48
|
"mobx": "^6.0.4",
|
|
49
|
+
"@opentelemetry/api": "1.9.0",
|
|
50
|
+
"@opentelemetry/exporter-trace-otlp-http": "0.53.0",
|
|
51
|
+
"@opentelemetry/resources": "1.26.0",
|
|
52
|
+
"@opentelemetry/sdk-trace-node": "1.26.0",
|
|
53
|
+
"@opentelemetry/semantic-conventions": "1.27.0",
|
|
49
54
|
"pluralize": "^8.0.0",
|
|
50
55
|
"react": "^17.0.0 || ^18.2.0 || ^19.0.0",
|
|
51
56
|
"react-dom": "^17.0.0 || ^18.2.0 || ^19.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getMergedConfig } from '@redocly/openapi-core';
|
|
2
2
|
import { handlePush } from '../../commands/push';
|
|
3
|
-
import { promptClientToken } from '../../commands/
|
|
3
|
+
import { promptClientToken } from '../../commands/auth';
|
|
4
4
|
import { ConfigFixture } from '../fixtures/config';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ jest.mock('fs', () => ({
|
|
|
23
23
|
|
|
24
24
|
// Mock OpenAPI core
|
|
25
25
|
jest.mock('@redocly/openapi-core');
|
|
26
|
-
jest.mock('../../commands/
|
|
26
|
+
jest.mock('../../commands/auth');
|
|
27
27
|
jest.mock('../../utils/miscellaneous');
|
|
28
28
|
|
|
29
29
|
const mockPromptClientToken = promptClientToken as jest.MockedFunction<typeof promptClientToken>;
|
|
@@ -502,6 +502,7 @@ describe('checkIfRulesetExist', () => {
|
|
|
502
502
|
async2: {},
|
|
503
503
|
async3: {},
|
|
504
504
|
arazzo1: {},
|
|
505
|
+
overlay1: {},
|
|
505
506
|
};
|
|
506
507
|
expect(() => checkIfRulesetExist(rules)).toThrowError(
|
|
507
508
|
'⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/'
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { RedoclyOAuthDeviceFlow } from '../device-flow';
|
|
2
|
+
|
|
3
|
+
jest.mock('child_process');
|
|
4
|
+
|
|
5
|
+
describe('RedoclyOAuthDeviceFlow', () => {
|
|
6
|
+
const mockBaseUrl = 'https://test.redocly.com';
|
|
7
|
+
const mockClientName = 'test-client';
|
|
8
|
+
const mockVersion = '1.0.0';
|
|
9
|
+
let flow: RedoclyOAuthDeviceFlow;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
flow = new RedoclyOAuthDeviceFlow(mockBaseUrl, mockClientName, mockVersion);
|
|
13
|
+
jest.resetAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('verifyToken', () => {
|
|
17
|
+
it('returns true for valid token', async () => {
|
|
18
|
+
jest.spyOn(flow['apiClient'], 'request').mockResolvedValue({
|
|
19
|
+
json: () => Promise.resolve({ user: { id: '123' } }),
|
|
20
|
+
} as Response);
|
|
21
|
+
|
|
22
|
+
const result = await flow.verifyToken('valid-token');
|
|
23
|
+
expect(result).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns false for invalid token', async () => {
|
|
27
|
+
jest.spyOn(flow['apiClient'], 'request').mockRejectedValue(new Error('Invalid token'));
|
|
28
|
+
const result = await flow.verifyToken('invalid-token');
|
|
29
|
+
expect(result).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('verifyApiKey', () => {
|
|
34
|
+
it('returns true for valid API key', async () => {
|
|
35
|
+
jest.spyOn(flow['apiClient'], 'request').mockResolvedValue({
|
|
36
|
+
json: () => Promise.resolve({ success: true }),
|
|
37
|
+
} as Response);
|
|
38
|
+
|
|
39
|
+
const result = await flow.verifyApiKey('valid-key');
|
|
40
|
+
expect(result).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns false for invalid API key', async () => {
|
|
44
|
+
jest.spyOn(flow['apiClient'], 'request').mockRejectedValue(new Error('Invalid API key'));
|
|
45
|
+
const result = await flow.verifyApiKey('invalid-key');
|
|
46
|
+
expect(result).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('refreshToken', () => {
|
|
51
|
+
it('successfully refreshes token', async () => {
|
|
52
|
+
const mockResponse = {
|
|
53
|
+
access_token: 'new-token',
|
|
54
|
+
refresh_token: 'new-refresh',
|
|
55
|
+
expires_in: 3600,
|
|
56
|
+
};
|
|
57
|
+
jest.spyOn(flow['apiClient'], 'request').mockResolvedValue({
|
|
58
|
+
json: () => Promise.resolve(mockResponse),
|
|
59
|
+
} as Response);
|
|
60
|
+
|
|
61
|
+
const result = await flow.refreshToken('old-refresh-token');
|
|
62
|
+
expect(result).toEqual(mockResponse);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws error when refresh fails', async () => {
|
|
66
|
+
jest.spyOn(flow['apiClient'], 'request').mockResolvedValue({
|
|
67
|
+
json: () => Promise.resolve({}),
|
|
68
|
+
} as Response);
|
|
69
|
+
|
|
70
|
+
await expect(flow.refreshToken('invalid-refresh')).rejects.toThrow('Failed to refresh token');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { RedoclyOAuthClient } from '../oauth-client';
|
|
2
|
+
import { RedoclyOAuthDeviceFlow } from '../device-flow';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
|
|
7
|
+
jest.mock('node:fs');
|
|
8
|
+
jest.mock('node:os');
|
|
9
|
+
jest.mock('../device-flow');
|
|
10
|
+
|
|
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: RedoclyOAuthClient;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.resetAllMocks();
|
|
21
|
+
(os.homedir as jest.Mock).mockReturnValue(mockHomeDir);
|
|
22
|
+
process.env.HOME = mockHomeDir;
|
|
23
|
+
client = new RedoclyOAuthClient(mockClientName, mockVersion);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('login', () => {
|
|
27
|
+
it('successfully logs in and saves token', async () => {
|
|
28
|
+
const mockToken = { access_token: 'test-token' };
|
|
29
|
+
const mockDeviceFlow = {
|
|
30
|
+
run: jest.fn().mockResolvedValue(mockToken),
|
|
31
|
+
};
|
|
32
|
+
(RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
|
|
33
|
+
|
|
34
|
+
await client.login(mockBaseUrl);
|
|
35
|
+
|
|
36
|
+
expect(mockDeviceFlow.run).toHaveBeenCalled();
|
|
37
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('throws error when login fails', async () => {
|
|
41
|
+
const mockDeviceFlow = {
|
|
42
|
+
run: jest.fn().mockResolvedValue(null),
|
|
43
|
+
};
|
|
44
|
+
(RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
|
|
45
|
+
|
|
46
|
+
await expect(client.login(mockBaseUrl)).rejects.toThrow('Failed to login');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('logout', () => {
|
|
51
|
+
it('removes token file if it exists', async () => {
|
|
52
|
+
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
|
53
|
+
|
|
54
|
+
await client.logout();
|
|
55
|
+
|
|
56
|
+
expect(fs.rmSync).toHaveBeenCalledWith(path.join(mockRedoclyDir, 'auth.json'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('silently fails if token file does not exist', async () => {
|
|
60
|
+
(fs.existsSync as jest.Mock).mockReturnValue(false);
|
|
61
|
+
|
|
62
|
+
await expect(client.logout()).resolves.not.toThrow();
|
|
63
|
+
expect(fs.rmSync).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('isAuthorized', () => {
|
|
68
|
+
it('verifies API key if provided', async () => {
|
|
69
|
+
const mockDeviceFlow = {
|
|
70
|
+
verifyApiKey: jest.fn().mockResolvedValue(true),
|
|
71
|
+
};
|
|
72
|
+
(RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
|
|
73
|
+
|
|
74
|
+
const result = await client.isAuthorized(mockBaseUrl, 'test-api-key');
|
|
75
|
+
|
|
76
|
+
expect(result).toBe(true);
|
|
77
|
+
expect(mockDeviceFlow.verifyApiKey).toHaveBeenCalledWith('test-api-key');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('verifies access token if no API key provided', async () => {
|
|
81
|
+
const mockToken = { access_token: 'test-token' };
|
|
82
|
+
const mockDeviceFlow = {
|
|
83
|
+
verifyToken: jest.fn().mockResolvedValue(true),
|
|
84
|
+
};
|
|
85
|
+
(RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
|
|
86
|
+
(fs.readFileSync as jest.Mock).mockReturnValue(
|
|
87
|
+
client['cipher'].update(JSON.stringify(mockToken), 'utf8', 'hex') +
|
|
88
|
+
client['cipher'].final('hex')
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const result = await client.isAuthorized(mockBaseUrl);
|
|
92
|
+
|
|
93
|
+
expect(result).toBe(true);
|
|
94
|
+
expect(mockDeviceFlow.verifyToken).toHaveBeenCalledWith('test-token');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns false if token refresh fails', async () => {
|
|
98
|
+
const mockToken = {
|
|
99
|
+
access_token: 'old-token',
|
|
100
|
+
refresh_token: 'refresh-token',
|
|
101
|
+
};
|
|
102
|
+
const mockDeviceFlow = {
|
|
103
|
+
verifyToken: jest.fn().mockResolvedValue(false),
|
|
104
|
+
refreshToken: jest.fn().mockRejectedValue(new Error('Refresh failed')),
|
|
105
|
+
};
|
|
106
|
+
(RedoclyOAuthDeviceFlow as jest.Mock).mockImplementation(() => mockDeviceFlow);
|
|
107
|
+
(fs.readFileSync as jest.Mock).mockReturnValue(
|
|
108
|
+
client['cipher'].update(JSON.stringify(mockToken), 'utf8', 'hex') +
|
|
109
|
+
client['cipher'].final('hex')
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const result = await client.isAuthorized(mockBaseUrl);
|
|
113
|
+
|
|
114
|
+
expect(result).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|