@redocly/cli 0.0.0-snapshot.1737627998 → 0.0.0-snapshot.1739895544
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/README.md +1 -1
- package/lib/__tests__/commands/push-region.test.js +47 -12
- package/lib/__tests__/commands/push.test.js +39 -6
- package/lib/__tests__/fetch-with-timeout.test.js +33 -13
- package/lib/__tests__/utils.test.js +1 -0
- package/lib/__tests__/wrapper.test.js +9 -1
- 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/build-docs/index.js +1 -1
- package/lib/commands/push.d.ts +1 -1
- package/lib/commands/push.js +15 -9
- package/lib/commands/split/index.js +4 -4
- package/lib/index.js +126 -15
- package/lib/otel.d.ts +10 -0
- package/lib/otel.js +47 -0
- package/lib/{cms → reunite}/api/__tests__/api.client.test.js +17 -13
- package/lib/reunite/api/__tests__/domains.test.js +32 -0
- package/lib/{cms → reunite}/api/api-client.d.ts +11 -1
- package/lib/{cms → reunite}/api/api-client.js +15 -4
- 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/fetch-with-timeout.d.ts +1 -2
- package/lib/utils/fetch-with-timeout.js +5 -7
- package/lib/utils/miscellaneous.d.ts +5 -4
- package/lib/utils/miscellaneous.js +14 -14
- package/package.json +17 -11
- 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/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/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/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Then you can use it as `redocly [command] [options]`, for example:
|
|
|
28
28
|
redocly lint path-to-root-file.yaml
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The minimum required versions of Node.js and NPM are
|
|
31
|
+
The minimum required versions of Node.js and NPM are 18.17.0 and 10.8.2 respectively.
|
|
32
32
|
|
|
33
33
|
### Docker
|
|
34
34
|
|
|
@@ -2,28 +2,61 @@
|
|
|
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
|
|
5
|
+
const auth_1 = require("../../commands/auth");
|
|
6
6
|
const config_1 = require("../fixtures/config");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
// Mock fs operations
|
|
9
|
+
jest.mock('fs', () => ({
|
|
10
|
+
...jest.requireActual('fs'),
|
|
11
|
+
createReadStream: () => {
|
|
12
|
+
const readable = new node_stream_1.Readable();
|
|
13
|
+
readable.push('test data');
|
|
14
|
+
readable.push(null);
|
|
15
|
+
return readable;
|
|
16
|
+
},
|
|
17
|
+
statSync: () => ({ size: 9 }),
|
|
18
|
+
readFileSync: () => Buffer.from('test data'),
|
|
19
|
+
existsSync: () => false,
|
|
20
|
+
readdirSync: () => [],
|
|
13
21
|
}));
|
|
22
|
+
openapi_core_1.getMergedConfig.mockImplementation((config) => config);
|
|
23
|
+
// Mock OpenAPI core
|
|
14
24
|
jest.mock('@redocly/openapi-core');
|
|
15
|
-
jest.mock('../../commands/
|
|
25
|
+
jest.mock('../../commands/auth');
|
|
16
26
|
jest.mock('../../utils/miscellaneous');
|
|
17
|
-
|
|
18
|
-
const mockPromptClientToken = login_1.promptClientToken;
|
|
27
|
+
const mockPromptClientToken = auth_1.promptClientToken;
|
|
19
28
|
describe('push-with-region', () => {
|
|
20
29
|
const redoclyClient = require('@redocly/openapi-core').__redoclyClient;
|
|
21
30
|
redoclyClient.isAuthorizedWithRedoclyByRegion = jest.fn().mockResolvedValue(false);
|
|
31
|
+
const originalFetch = fetch;
|
|
22
32
|
beforeAll(() => {
|
|
33
|
+
// Mock global fetch
|
|
34
|
+
global.fetch = jest.fn(() => Promise.resolve({
|
|
35
|
+
ok: true,
|
|
36
|
+
status: 200,
|
|
37
|
+
json: () => Promise.resolve({}),
|
|
38
|
+
headers: new Headers(),
|
|
39
|
+
statusText: 'OK',
|
|
40
|
+
redirected: false,
|
|
41
|
+
type: 'default',
|
|
42
|
+
url: '',
|
|
43
|
+
clone: () => ({}),
|
|
44
|
+
body: new ReadableStream(),
|
|
45
|
+
bodyUsed: false,
|
|
46
|
+
arrayBuffer: async () => new ArrayBuffer(0),
|
|
47
|
+
blob: async () => new Blob(),
|
|
48
|
+
formData: async () => new FormData(),
|
|
49
|
+
text: async () => '',
|
|
50
|
+
}));
|
|
51
|
+
});
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
global.fetch = originalFetch;
|
|
54
|
+
});
|
|
55
|
+
beforeEach(() => {
|
|
23
56
|
jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
24
57
|
});
|
|
25
58
|
it('should call login with default domain when region is US', async () => {
|
|
26
|
-
redoclyClient.domain = '
|
|
59
|
+
redoclyClient.domain = 'redocly.com';
|
|
27
60
|
await (0, push_1.handlePush)({
|
|
28
61
|
argv: {
|
|
29
62
|
upsert: true,
|
|
@@ -39,6 +72,8 @@ describe('push-with-region', () => {
|
|
|
39
72
|
});
|
|
40
73
|
it('should call login with EU domain when region is EU', async () => {
|
|
41
74
|
redoclyClient.domain = 'eu.redocly.com';
|
|
75
|
+
// Update config for EU region
|
|
76
|
+
const euConfig = { ...config_1.ConfigFixture, region: 'eu' };
|
|
42
77
|
await (0, push_1.handlePush)({
|
|
43
78
|
argv: {
|
|
44
79
|
upsert: true,
|
|
@@ -46,7 +81,7 @@ describe('push-with-region', () => {
|
|
|
46
81
|
destination: '@org/my-api@1.0.0',
|
|
47
82
|
branchName: 'test',
|
|
48
83
|
},
|
|
49
|
-
config:
|
|
84
|
+
config: euConfig,
|
|
50
85
|
version: 'cli-version',
|
|
51
86
|
});
|
|
52
87
|
expect(mockPromptClientToken).toBeCalledTimes(1);
|
|
@@ -6,21 +6,54 @@ const miscellaneous_1 = require("../../utils/miscellaneous");
|
|
|
6
6
|
const push_1 = require("../../commands/push");
|
|
7
7
|
const config_1 = require("../fixtures/config");
|
|
8
8
|
const colorette_1 = require("colorette");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const node_stream_1 = require("node:stream");
|
|
10
|
+
// Mock fs operations
|
|
11
|
+
jest.mock('fs', () => ({
|
|
12
|
+
...jest.requireActual('fs'),
|
|
13
|
+
createReadStream: jest.fn(() => {
|
|
14
|
+
const readable = new node_stream_1.Readable();
|
|
15
|
+
readable.push('test data');
|
|
16
|
+
readable.push(null);
|
|
17
|
+
return readable;
|
|
18
|
+
}),
|
|
19
|
+
statSync: jest.fn(() => ({ isDirectory: () => false, size: 10 })),
|
|
20
|
+
readFileSync: jest.fn(() => Buffer.from('test data')),
|
|
21
|
+
existsSync: jest.fn(() => false),
|
|
22
|
+
readdirSync: jest.fn(() => []),
|
|
15
23
|
}));
|
|
16
24
|
jest.mock('@redocly/openapi-core');
|
|
17
25
|
jest.mock('../../utils/miscellaneous');
|
|
26
|
+
// Mock fetch
|
|
27
|
+
const mockFetch = jest.fn(() => Promise.resolve({
|
|
28
|
+
ok: true,
|
|
29
|
+
status: 200,
|
|
30
|
+
json: () => Promise.resolve({}),
|
|
31
|
+
headers: new Headers(),
|
|
32
|
+
statusText: 'OK',
|
|
33
|
+
redirected: false,
|
|
34
|
+
type: 'default',
|
|
35
|
+
url: '',
|
|
36
|
+
clone: () => ({}),
|
|
37
|
+
body: null,
|
|
38
|
+
bodyUsed: false,
|
|
39
|
+
arrayBuffer: async () => new ArrayBuffer(0),
|
|
40
|
+
blob: async () => new Blob(),
|
|
41
|
+
formData: async () => new FormData(),
|
|
42
|
+
text: async () => '',
|
|
43
|
+
}));
|
|
44
|
+
const originalFetch = global.fetch;
|
|
18
45
|
openapi_core_1.getMergedConfig.mockImplementation((config) => config);
|
|
19
46
|
describe('push', () => {
|
|
20
47
|
const redoclyClient = require('@redocly/openapi-core').__redoclyClient;
|
|
48
|
+
beforeAll(() => {
|
|
49
|
+
global.fetch = mockFetch;
|
|
50
|
+
});
|
|
21
51
|
beforeEach(() => {
|
|
22
52
|
jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
23
53
|
});
|
|
54
|
+
afterAll(() => {
|
|
55
|
+
global.fetch = originalFetch;
|
|
56
|
+
});
|
|
24
57
|
it('pushes definition', async () => {
|
|
25
58
|
await (0, push_1.handlePush)({
|
|
26
59
|
argv: {
|
|
@@ -2,11 +2,31 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const abort_controller_1 = require("abort-controller");
|
|
4
4
|
const fetch_with_timeout_1 = require("../utils/fetch-with-timeout");
|
|
5
|
-
const node_fetch_1 = require("node-fetch");
|
|
6
5
|
const openapi_core_1 = require("@redocly/openapi-core");
|
|
7
6
|
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
8
|
-
jest.mock('node-fetch');
|
|
9
7
|
jest.mock('@redocly/openapi-core');
|
|
8
|
+
const signalInstance = new abort_controller_1.default().signal;
|
|
9
|
+
const mockFetch = jest.fn(() => Promise.resolve({
|
|
10
|
+
ok: true,
|
|
11
|
+
status: 200,
|
|
12
|
+
json: () => Promise.resolve({}),
|
|
13
|
+
headers: new Headers(),
|
|
14
|
+
statusText: 'OK',
|
|
15
|
+
redirected: false,
|
|
16
|
+
type: 'default',
|
|
17
|
+
url: '',
|
|
18
|
+
clone: () => ({}),
|
|
19
|
+
body: null,
|
|
20
|
+
bodyUsed: false,
|
|
21
|
+
arrayBuffer: async () => new ArrayBuffer(0),
|
|
22
|
+
blob: async () => new Blob(),
|
|
23
|
+
formData: async () => new FormData(),
|
|
24
|
+
text: async () => '',
|
|
25
|
+
signal: signalInstance,
|
|
26
|
+
dispatcher: undefined,
|
|
27
|
+
}));
|
|
28
|
+
const originalFetch = global.fetch;
|
|
29
|
+
global.fetch = mockFetch;
|
|
10
30
|
describe('fetchWithTimeout', () => {
|
|
11
31
|
beforeAll(() => {
|
|
12
32
|
// @ts-ignore
|
|
@@ -16,29 +36,29 @@ describe('fetchWithTimeout', () => {
|
|
|
16
36
|
beforeEach(() => {
|
|
17
37
|
openapi_core_1.getProxyAgent.mockReturnValueOnce(undefined);
|
|
18
38
|
});
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
afterAll(() => {
|
|
40
|
+
global.fetch = originalFetch;
|
|
21
41
|
});
|
|
22
|
-
it('should call
|
|
42
|
+
it('should call fetch with signal', async () => {
|
|
23
43
|
await (0, fetch_with_timeout_1.default)('url', { timeout: 1000 });
|
|
24
44
|
expect(global.setTimeout).toHaveBeenCalledTimes(1);
|
|
25
|
-
expect(
|
|
26
|
-
signal:
|
|
27
|
-
|
|
28
|
-
});
|
|
45
|
+
expect(global.fetch).toHaveBeenCalledWith('url', expect.objectContaining({
|
|
46
|
+
signal: expect.any(AbortSignal),
|
|
47
|
+
dispatcher: undefined,
|
|
48
|
+
}));
|
|
29
49
|
expect(global.clearTimeout).toHaveBeenCalledTimes(1);
|
|
30
50
|
});
|
|
31
|
-
it('should call
|
|
51
|
+
it('should call fetch with proxy agent', async () => {
|
|
32
52
|
openapi_core_1.getProxyAgent.mockRestore();
|
|
33
53
|
const proxyAgent = new https_proxy_agent_1.HttpsProxyAgent('http://localhost');
|
|
34
54
|
openapi_core_1.getProxyAgent.mockReturnValueOnce(proxyAgent);
|
|
35
55
|
await (0, fetch_with_timeout_1.default)('url');
|
|
36
|
-
expect(
|
|
56
|
+
expect(global.fetch).toHaveBeenCalledWith('url', { dispatcher: proxyAgent });
|
|
37
57
|
});
|
|
38
|
-
it('should call
|
|
58
|
+
it('should call fetch without signal when timeout is not passed', async () => {
|
|
39
59
|
await (0, fetch_with_timeout_1.default)('url');
|
|
40
60
|
expect(global.setTimeout).not.toHaveBeenCalled();
|
|
41
|
-
expect(
|
|
61
|
+
expect(global.fetch).toHaveBeenCalledWith('url', { agent: undefined });
|
|
42
62
|
expect(global.clearTimeout).not.toHaveBeenCalled();
|
|
43
63
|
});
|
|
44
64
|
});
|
|
@@ -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
|
});
|
|
@@ -6,11 +6,19 @@ const wrapper_1 = require("../wrapper");
|
|
|
6
6
|
const lint_1 = require("../commands/lint");
|
|
7
7
|
const push_1 = require("../commands/push");
|
|
8
8
|
const openapi_core_1 = require("@redocly/openapi-core");
|
|
9
|
-
jest.
|
|
9
|
+
const mockFetch = jest.fn();
|
|
10
|
+
const originalFetch = global.fetch;
|
|
10
11
|
jest.mock('../utils/miscellaneous', () => ({
|
|
11
12
|
sendTelemetry: jest.fn(),
|
|
12
13
|
loadConfigAndHandleErrors: jest.fn(),
|
|
13
14
|
}));
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
global.fetch = mockFetch;
|
|
17
|
+
});
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
jest.resetAllMocks();
|
|
20
|
+
global.fetch = originalFetch;
|
|
21
|
+
});
|
|
14
22
|
jest.mock('../commands/lint', () => ({
|
|
15
23
|
handleLint: jest.fn().mockImplementation(({ collectSpecData }) => {
|
|
16
24
|
collectSpecData({ openapi: '3.1.0' });
|
|
@@ -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
|
+
}
|