@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.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/lib/__tests__/commands/push-region.test.js +47 -12
  3. package/lib/__tests__/commands/push.test.js +39 -6
  4. package/lib/__tests__/fetch-with-timeout.test.js +33 -13
  5. package/lib/__tests__/utils.test.js +1 -0
  6. package/lib/__tests__/wrapper.test.js +9 -1
  7. package/lib/auth/__tests__/device-flow.test.js +62 -0
  8. package/lib/auth/__tests__/oauth-client.test.js +93 -0
  9. package/lib/auth/device-flow.d.ts +26 -0
  10. package/lib/auth/device-flow.js +133 -0
  11. package/lib/auth/oauth-client.d.ts +14 -0
  12. package/lib/auth/oauth-client.js +93 -0
  13. package/lib/commands/auth.d.ts +13 -0
  14. package/lib/commands/auth.js +51 -0
  15. package/lib/commands/build-docs/index.js +1 -1
  16. package/lib/commands/push.d.ts +1 -1
  17. package/lib/commands/push.js +15 -9
  18. package/lib/commands/split/index.js +4 -4
  19. package/lib/index.js +126 -15
  20. package/lib/otel.d.ts +10 -0
  21. package/lib/otel.js +47 -0
  22. package/lib/{cms → reunite}/api/__tests__/api.client.test.js +17 -13
  23. package/lib/reunite/api/__tests__/domains.test.js +32 -0
  24. package/lib/{cms → reunite}/api/api-client.d.ts +11 -1
  25. package/lib/{cms → reunite}/api/api-client.js +15 -4
  26. package/lib/reunite/api/domains.d.ts +4 -0
  27. package/lib/reunite/api/domains.js +22 -0
  28. package/lib/reunite/commands/__tests__/push.test.d.ts +1 -0
  29. package/lib/reunite/commands/__tests__/utils.test.d.ts +1 -0
  30. package/lib/types.d.ts +4 -4
  31. package/lib/utils/fetch-with-timeout.d.ts +1 -2
  32. package/lib/utils/fetch-with-timeout.js +5 -7
  33. package/lib/utils/miscellaneous.d.ts +5 -4
  34. package/lib/utils/miscellaneous.js +14 -14
  35. package/package.json +17 -11
  36. package/lib/cms/api/__tests__/domains.test.js +0 -13
  37. package/lib/cms/api/domains.d.ts +0 -1
  38. package/lib/cms/api/domains.js +0 -11
  39. package/lib/commands/login.d.ts +0 -9
  40. package/lib/commands/login.js +0 -23
  41. /package/lib/{cms/api/__tests__/api-keys.test.d.ts → auth/__tests__/device-flow.test.d.ts} +0 -0
  42. /package/lib/{cms/api/__tests__/api.client.test.d.ts → auth/__tests__/oauth-client.test.d.ts} +0 -0
  43. /package/lib/{cms/api/__tests__/domains.test.d.ts → reunite/api/__tests__/api-keys.test.d.ts} +0 -0
  44. /package/lib/{cms → reunite}/api/__tests__/api-keys.test.js +0 -0
  45. /package/lib/{cms/commands/__tests__/push-status.test.d.ts → reunite/api/__tests__/api.client.test.d.ts} +0 -0
  46. /package/lib/{cms/commands/__tests__/push.test.d.ts → reunite/api/__tests__/domains.test.d.ts} +0 -0
  47. /package/lib/{cms → reunite}/api/api-keys.d.ts +0 -0
  48. /package/lib/{cms → reunite}/api/api-keys.js +0 -0
  49. /package/lib/{cms → reunite}/api/index.d.ts +0 -0
  50. /package/lib/{cms → reunite}/api/index.js +0 -0
  51. /package/lib/{cms → reunite}/api/types.d.ts +0 -0
  52. /package/lib/{cms → reunite}/api/types.js +0 -0
  53. /package/lib/{cms/commands/__tests__/utils.test.d.ts → reunite/commands/__tests__/push-status.test.d.ts} +0 -0
  54. /package/lib/{cms → reunite}/commands/__tests__/push-status.test.js +0 -0
  55. /package/lib/{cms → reunite}/commands/__tests__/push.test.js +0 -0
  56. /package/lib/{cms → reunite}/commands/__tests__/utils.test.js +0 -0
  57. /package/lib/{cms → reunite}/commands/push-status.d.ts +0 -0
  58. /package/lib/{cms → reunite}/commands/push-status.js +0 -0
  59. /package/lib/{cms → reunite}/commands/push.d.ts +0 -0
  60. /package/lib/{cms → reunite}/commands/push.js +0 -0
  61. /package/lib/{cms → reunite}/commands/utils.d.ts +0 -0
  62. /package/lib/{cms → reunite}/commands/utils.js +0 -0
  63. /package/lib/{cms → reunite}/utils.d.ts +0 -0
  64. /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 14.19.0 and 7.0.0 respectively.
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 login_1 = require("../../commands/login");
5
+ const auth_1 = require("../../commands/auth");
6
6
  const config_1 = require("../fixtures/config");
7
- jest.mock('fs');
8
- jest.mock('node-fetch', () => ({
9
- default: jest.fn(() => ({
10
- ok: true,
11
- json: jest.fn().mockResolvedValue({}),
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/login');
25
+ jest.mock('../../commands/auth');
16
26
  jest.mock('../../utils/miscellaneous');
17
- openapi_core_1.getMergedConfig.mockImplementation((config) => config);
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 = 'redoc.ly';
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: config_1.ConfigFixture,
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
- jest.mock('fs');
10
- jest.mock('node-fetch', () => ({
11
- default: jest.fn(() => ({
12
- ok: true,
13
- json: jest.fn().mockResolvedValue({}),
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
- afterEach(() => {
20
- jest.clearAllMocks();
39
+ afterAll(() => {
40
+ global.fetch = originalFetch;
21
41
  });
22
- it('should call node-fetch with signal', async () => {
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(node_fetch_1.default).toHaveBeenCalledWith('url', {
26
- signal: new abort_controller_1.default().signal,
27
- agent: undefined,
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 node-fetch with proxy agent', async () => {
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(node_fetch_1.default).toHaveBeenCalledWith('url', { agent: proxyAgent });
56
+ expect(global.fetch).toHaveBeenCalledWith('url', { dispatcher: proxyAgent });
37
57
  });
38
- it('should call node-fetch without signal when timeout is not passed', async () => {
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(node_fetch_1.default).toHaveBeenCalledWith('url', { agent: undefined });
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.mock('node-fetch');
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
+ }