@mintlify/cli 4.0.1098 → 4.0.1099
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/__test__/analytics/client.test.ts +28 -20
- package/__test__/authenticatedFetch.test.ts +182 -0
- package/__test__/keyring.test.ts +7 -0
- package/bin/analytics/client.js +3 -20
- package/bin/authenticatedFetch.js +46 -0
- package/bin/keyring.js +6 -0
- package/bin/status.js +2 -3
- package/bin/tokenRefresh.js +50 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/analytics/client.ts +3 -20
- package/src/authenticatedFetch.ts +42 -0
- package/src/keyring.ts +5 -0
- package/src/status.tsx +2 -3
- package/src/tokenRefresh.ts +48 -0
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
|
|
13
13
|
vi.mock('../../src/keyring.js', () => ({
|
|
14
14
|
getAccessToken: vi.fn().mockResolvedValue(null),
|
|
15
|
+
getRefreshToken: vi.fn().mockResolvedValue(null),
|
|
16
|
+
storeCredentials: vi.fn().mockResolvedValue(undefined),
|
|
15
17
|
}));
|
|
16
18
|
|
|
17
19
|
const mockFetch = vi.fn();
|
|
@@ -30,6 +32,7 @@ afterEach(() => {
|
|
|
30
32
|
function mockOk(data: unknown) {
|
|
31
33
|
mockFetch.mockResolvedValueOnce({
|
|
32
34
|
ok: true,
|
|
35
|
+
status: 200,
|
|
33
36
|
json: () => Promise.resolve(data),
|
|
34
37
|
});
|
|
35
38
|
}
|
|
@@ -43,8 +46,13 @@ function mockError(status: number, body: string) {
|
|
|
43
46
|
});
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
function calledUrl():
|
|
47
|
-
|
|
49
|
+
function calledUrl(): string {
|
|
50
|
+
const arg = mockFetch.mock.calls[0]![0];
|
|
51
|
+
return typeof arg === 'string' ? arg : String(arg);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function calledUrlObj(): URL {
|
|
55
|
+
return new URL(calledUrl());
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
describe('client auth', () => {
|
|
@@ -59,7 +67,7 @@ describe('client auth', () => {
|
|
|
59
67
|
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
60
68
|
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'test');
|
|
61
69
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
62
|
-
expect.any(
|
|
70
|
+
expect.any(String),
|
|
63
71
|
expect.objectContaining({
|
|
64
72
|
headers: expect.objectContaining({
|
|
65
73
|
Authorization: 'Bearer test-token',
|
|
@@ -80,14 +88,14 @@ describe('client request handling', () => {
|
|
|
80
88
|
it('passes subdomain as query param when provided', async () => {
|
|
81
89
|
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
82
90
|
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'my-docs');
|
|
83
|
-
expect(
|
|
84
|
-
expect(
|
|
91
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('my-docs');
|
|
92
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/feedback');
|
|
85
93
|
});
|
|
86
94
|
|
|
87
95
|
it('omits subdomain param when not provided', async () => {
|
|
88
96
|
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
89
97
|
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' });
|
|
90
|
-
expect(
|
|
98
|
+
expect(calledUrlObj().searchParams.has('subdomain')).toBe(false);
|
|
91
99
|
});
|
|
92
100
|
|
|
93
101
|
it('sets query params and omits undefined values', async () => {
|
|
@@ -98,9 +106,9 @@ describe('client request handling', () => {
|
|
|
98
106
|
limit: 10,
|
|
99
107
|
cursor: undefined,
|
|
100
108
|
});
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
103
|
-
expect(
|
|
109
|
+
expect(calledUrlObj().searchParams.get('dateFrom')).toBe('2024-01-01');
|
|
110
|
+
expect(calledUrlObj().searchParams.get('limit')).toBe('10');
|
|
111
|
+
expect(calledUrlObj().searchParams.has('cursor')).toBe(false);
|
|
104
112
|
});
|
|
105
113
|
});
|
|
106
114
|
|
|
@@ -108,51 +116,51 @@ describe('endpoint paths', () => {
|
|
|
108
116
|
it('getKpi', async () => {
|
|
109
117
|
mockOk({ humanVisitors: 0 });
|
|
110
118
|
await getKpi({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
111
|
-
expect(
|
|
112
|
-
expect(
|
|
119
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/kpi');
|
|
120
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
113
121
|
});
|
|
114
122
|
|
|
115
123
|
it('getFeedbackByPage', async () => {
|
|
116
124
|
mockOk({ feedback: [], hasMore: false });
|
|
117
125
|
await getFeedbackByPage({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
118
|
-
expect(
|
|
126
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/feedback/by-page');
|
|
119
127
|
});
|
|
120
128
|
|
|
121
129
|
it('getConversations', async () => {
|
|
122
130
|
mockOk({ conversations: [], nextCursor: null, hasMore: false });
|
|
123
131
|
await getConversations({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
124
|
-
expect(
|
|
132
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/assistant');
|
|
125
133
|
});
|
|
126
134
|
|
|
127
135
|
it('getSearches', async () => {
|
|
128
136
|
mockOk({ searches: [], totalSearches: 0, nextCursor: null });
|
|
129
137
|
await getSearches({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
130
|
-
expect(
|
|
138
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/searches');
|
|
131
139
|
});
|
|
132
140
|
|
|
133
141
|
it('getViews', async () => {
|
|
134
142
|
mockOk({ totals: {}, views: [], hasMore: false });
|
|
135
143
|
await getViews({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
136
|
-
expect(
|
|
144
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/views');
|
|
137
145
|
});
|
|
138
146
|
|
|
139
147
|
it('getVisitors', async () => {
|
|
140
148
|
mockOk({ totals: {}, visitors: [], hasMore: false });
|
|
141
149
|
await getVisitors({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
142
|
-
expect(
|
|
150
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/visitors');
|
|
143
151
|
});
|
|
144
152
|
|
|
145
153
|
it('getBuckets', async () => {
|
|
146
154
|
mockOk({ data: [], pagination: { total: 0 } });
|
|
147
155
|
await getBuckets({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
148
|
-
expect(
|
|
149
|
-
expect(
|
|
156
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/conversations/buckets');
|
|
157
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
150
158
|
});
|
|
151
159
|
|
|
152
160
|
it('getBucketThreads', async () => {
|
|
153
161
|
mockOk({ data: [], pagination: { total: 0, hasMore: false, nextCursor: null } });
|
|
154
162
|
await getBucketThreads('bucket-123', { dateFrom: '2024-01-01' }, 'docs');
|
|
155
|
-
expect(
|
|
156
|
-
expect(
|
|
163
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/conversations/buckets/bucket-123');
|
|
164
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
157
165
|
});
|
|
158
166
|
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { authenticatedFetch } from '../src/authenticatedFetch.js';
|
|
4
|
+
|
|
5
|
+
const mockGetAccessToken = vi.fn();
|
|
6
|
+
const mockGetRefreshToken = vi.fn();
|
|
7
|
+
const mockStoreCredentials = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock('../src/keyring.js', () => ({
|
|
10
|
+
getAccessToken: (...args: unknown[]) => mockGetAccessToken(...args),
|
|
11
|
+
getRefreshToken: (...args: unknown[]) => mockGetRefreshToken(...args),
|
|
12
|
+
storeCredentials: (...args: unknown[]) => mockStoreCredentials(...args),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockFetch = vi.fn();
|
|
16
|
+
global.fetch = mockFetch;
|
|
17
|
+
|
|
18
|
+
function makeResponse(status: number, body: unknown = {}) {
|
|
19
|
+
return {
|
|
20
|
+
ok: status >= 200 && status < 300,
|
|
21
|
+
status,
|
|
22
|
+
statusText: status === 401 ? 'Unauthorized' : 'OK',
|
|
23
|
+
json: () => Promise.resolve(body),
|
|
24
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('authenticatedFetch', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
mockFetch.mockReset();
|
|
32
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', '');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.unstubAllEnvs();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sends access token from keyring', async () => {
|
|
40
|
+
mockGetAccessToken.mockResolvedValue('access-123');
|
|
41
|
+
mockFetch.mockResolvedValueOnce(makeResponse(200, { ok: true }));
|
|
42
|
+
|
|
43
|
+
await authenticatedFetch('http://test/api');
|
|
44
|
+
|
|
45
|
+
expect(mockFetch).toHaveBeenCalledWith('http://test/api', {
|
|
46
|
+
headers: { Authorization: 'Bearer access-123' },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('falls back to MINTLIFY_SESSION_TOKEN env var', async () => {
|
|
51
|
+
mockGetAccessToken.mockResolvedValue(null);
|
|
52
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'env-token');
|
|
53
|
+
mockFetch.mockResolvedValueOnce(makeResponse(200));
|
|
54
|
+
|
|
55
|
+
await authenticatedFetch('http://test/api');
|
|
56
|
+
|
|
57
|
+
expect(mockFetch).toHaveBeenCalledWith('http://test/api', {
|
|
58
|
+
headers: { Authorization: 'Bearer env-token' },
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws when no token is available', async () => {
|
|
63
|
+
mockGetAccessToken.mockResolvedValue(null);
|
|
64
|
+
|
|
65
|
+
await expect(authenticatedFetch('http://test/api')).rejects.toThrow('Not authenticated');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('refreshes token on 401 and retries', async () => {
|
|
69
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
70
|
+
mockGetRefreshToken.mockResolvedValue('refresh-123');
|
|
71
|
+
mockStoreCredentials.mockResolvedValue(undefined);
|
|
72
|
+
|
|
73
|
+
mockFetch
|
|
74
|
+
.mockResolvedValueOnce(makeResponse(401))
|
|
75
|
+
.mockResolvedValueOnce(
|
|
76
|
+
makeResponse(200, {
|
|
77
|
+
access_token: 'new-access',
|
|
78
|
+
refresh_token: 'new-refresh',
|
|
79
|
+
token_type: 'bearer',
|
|
80
|
+
expires_in: 3600,
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
.mockResolvedValueOnce(makeResponse(200, { ok: true }));
|
|
84
|
+
|
|
85
|
+
const res = await authenticatedFetch('http://test/api');
|
|
86
|
+
|
|
87
|
+
expect(res.status).toBe(200);
|
|
88
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
89
|
+
expect(mockFetch).toHaveBeenLastCalledWith('http://test/api', {
|
|
90
|
+
headers: { Authorization: 'Bearer new-access' },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('falls back to env token when refresh fails', async () => {
|
|
95
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
96
|
+
mockGetRefreshToken.mockResolvedValue('bad-refresh');
|
|
97
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'env-token');
|
|
98
|
+
|
|
99
|
+
mockFetch
|
|
100
|
+
.mockResolvedValueOnce(makeResponse(401))
|
|
101
|
+
.mockResolvedValueOnce(makeResponse(400, { error: 'invalid_grant' }))
|
|
102
|
+
.mockResolvedValueOnce(makeResponse(200, { ok: true }));
|
|
103
|
+
|
|
104
|
+
const res = await authenticatedFetch('http://test/api');
|
|
105
|
+
|
|
106
|
+
expect(res.status).toBe(200);
|
|
107
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
108
|
+
expect(mockFetch).toHaveBeenLastCalledWith('http://test/api', {
|
|
109
|
+
headers: { Authorization: 'Bearer env-token' },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns original 401 when refresh fails and no env token', async () => {
|
|
114
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
115
|
+
mockGetRefreshToken.mockResolvedValue('bad-refresh');
|
|
116
|
+
|
|
117
|
+
mockFetch
|
|
118
|
+
.mockResolvedValueOnce(makeResponse(401))
|
|
119
|
+
.mockResolvedValueOnce(makeResponse(400, { error: 'invalid_grant' }));
|
|
120
|
+
|
|
121
|
+
const res = await authenticatedFetch('http://test/api');
|
|
122
|
+
|
|
123
|
+
expect(res.status).toBe(401);
|
|
124
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('falls back to env token when no refresh token exists', async () => {
|
|
128
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
129
|
+
mockGetRefreshToken.mockResolvedValue(null);
|
|
130
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'env-token');
|
|
131
|
+
|
|
132
|
+
mockFetch
|
|
133
|
+
.mockResolvedValueOnce(makeResponse(401))
|
|
134
|
+
.mockResolvedValueOnce(makeResponse(200, { ok: true }));
|
|
135
|
+
|
|
136
|
+
const res = await authenticatedFetch('http://test/api');
|
|
137
|
+
|
|
138
|
+
expect(res.status).toBe(200);
|
|
139
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
140
|
+
expect(mockFetch).toHaveBeenLastCalledWith('http://test/api', {
|
|
141
|
+
headers: { Authorization: 'Bearer env-token' },
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns original 401 when no refresh token and no env token', async () => {
|
|
146
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
147
|
+
mockGetRefreshToken.mockResolvedValue(null);
|
|
148
|
+
|
|
149
|
+
mockFetch.mockResolvedValueOnce(makeResponse(401));
|
|
150
|
+
|
|
151
|
+
const res = await authenticatedFetch('http://test/api');
|
|
152
|
+
|
|
153
|
+
expect(res.status).toBe(401);
|
|
154
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not attempt refresh when only env token is available', async () => {
|
|
158
|
+
mockGetAccessToken.mockResolvedValue(null);
|
|
159
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'env-token');
|
|
160
|
+
|
|
161
|
+
mockFetch.mockResolvedValueOnce(makeResponse(401));
|
|
162
|
+
|
|
163
|
+
const res = await authenticatedFetch('http://test/api');
|
|
164
|
+
|
|
165
|
+
expect(res.status).toBe(401);
|
|
166
|
+
expect(mockGetRefreshToken).not.toHaveBeenCalled();
|
|
167
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('merges custom headers with auth header', async () => {
|
|
171
|
+
mockGetAccessToken.mockResolvedValue('token-123');
|
|
172
|
+
mockFetch.mockResolvedValueOnce(makeResponse(200));
|
|
173
|
+
|
|
174
|
+
await authenticatedFetch('http://test/api', {
|
|
175
|
+
headers: { Accept: 'application/json' },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(mockFetch).toHaveBeenCalledWith('http://test/api', {
|
|
179
|
+
headers: { Accept: 'application/json', Authorization: 'Bearer token-123' },
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
package/__test__/keyring.test.ts
CHANGED
|
@@ -33,6 +33,13 @@ describe('keyring', () => {
|
|
|
33
33
|
expect(mockKeytar.getPassword).toHaveBeenCalledWith('mintlify', 'access_token');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it('retrieves the refresh token via keytar', async () => {
|
|
37
|
+
const { getRefreshToken } = await import('../src/keyring.js');
|
|
38
|
+
const token = await getRefreshToken();
|
|
39
|
+
expect(token).toBe('test-token');
|
|
40
|
+
expect(mockKeytar.getPassword).toHaveBeenCalledWith('mintlify', 'refresh_token');
|
|
41
|
+
});
|
|
42
|
+
|
|
36
43
|
it('deletes both tokens via keytar', async () => {
|
|
37
44
|
const { clearCredentials } = await import('../src/keyring.js');
|
|
38
45
|
await clearCredentials();
|
package/bin/analytics/client.js
CHANGED
|
@@ -7,24 +7,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { authenticatedFetch } from '../authenticatedFetch.js';
|
|
10
11
|
import { API_URL } from '../constants.js';
|
|
11
|
-
function getAuthHeaders() {
|
|
12
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
13
|
-
try {
|
|
14
|
-
const { getAccessToken } = yield import('../keyring.js');
|
|
15
|
-
const token = yield getAccessToken();
|
|
16
|
-
if (token) {
|
|
17
|
-
return { Authorization: `Bearer ${token}` };
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
catch (_a) { }
|
|
21
|
-
const envToken = process.env.MINTLIFY_SESSION_TOKEN;
|
|
22
|
-
if (envToken) {
|
|
23
|
-
return { Authorization: `Bearer ${envToken}` };
|
|
24
|
-
}
|
|
25
|
-
throw new Error('Not authenticated. Run `mint login` to authenticate.');
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
12
|
function request(path_1) {
|
|
29
13
|
return __awaiter(this, arguments, void 0, function* (path, params = {}) {
|
|
30
14
|
const url = new URL(`${API_URL}/api/cli/analytics${path}`);
|
|
@@ -32,9 +16,8 @@ function request(path_1) {
|
|
|
32
16
|
if (value !== undefined)
|
|
33
17
|
url.searchParams.set(key, String(value));
|
|
34
18
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
headers: Object.assign(Object.assign({}, authHeaders), { Accept: 'application/json' }),
|
|
19
|
+
const res = yield authenticatedFetch(url.toString(), {
|
|
20
|
+
headers: { Accept: 'application/json' },
|
|
38
21
|
});
|
|
39
22
|
if (!res.ok) {
|
|
40
23
|
const body = yield res.text().catch(() => '');
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { getAccessToken } from './keyring.js';
|
|
11
|
+
import { refreshAccessToken } from './tokenRefresh.js';
|
|
12
|
+
function getKeyringToken() {
|
|
13
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
try {
|
|
15
|
+
return yield getAccessToken();
|
|
16
|
+
}
|
|
17
|
+
catch (_a) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function authenticatedFetch(url, init) {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
const keyringToken = yield getKeyringToken();
|
|
25
|
+
const envToken = process.env.MINTLIFY_SESSION_TOKEN;
|
|
26
|
+
const token = keyringToken !== null && keyringToken !== void 0 ? keyringToken : envToken;
|
|
27
|
+
if (!token) {
|
|
28
|
+
throw new Error('Not authenticated. Run `mint login` to authenticate.');
|
|
29
|
+
}
|
|
30
|
+
const makeRequest = (t) => fetch(url, Object.assign(Object.assign({}, init), { headers: Object.assign(Object.assign({}, init === null || init === void 0 ? void 0 : init.headers), { Authorization: `Bearer ${t}` }) }));
|
|
31
|
+
const res = yield makeRequest(token);
|
|
32
|
+
if (res.status !== 401)
|
|
33
|
+
return res;
|
|
34
|
+
// If the keyring token expired, try refreshing it
|
|
35
|
+
if (keyringToken) {
|
|
36
|
+
const refreshed = yield refreshAccessToken();
|
|
37
|
+
if (refreshed)
|
|
38
|
+
return makeRequest(refreshed);
|
|
39
|
+
}
|
|
40
|
+
// Fall back to the env session token if it wasn't already used
|
|
41
|
+
if (envToken && token !== envToken) {
|
|
42
|
+
return makeRequest(envToken);
|
|
43
|
+
}
|
|
44
|
+
return res;
|
|
45
|
+
});
|
|
46
|
+
}
|
package/bin/keyring.js
CHANGED
|
@@ -38,6 +38,12 @@ export function getAccessToken() {
|
|
|
38
38
|
return keytar.getPassword(SERVICE, ACCESS_TOKEN_ACCOUNT);
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
export function getRefreshToken() {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
const keytar = yield getKeytar();
|
|
44
|
+
return keytar.getPassword(SERVICE, REFRESH_TOKEN_ACCOUNT);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
41
47
|
export function clearCredentials() {
|
|
42
48
|
return __awaiter(this, void 0, void 0, function* () {
|
|
43
49
|
const keytar = yield getKeytar();
|
package/bin/status.js
CHANGED
|
@@ -11,6 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
import { addLog, ErrorLog } from '@mintlify/previewing';
|
|
12
12
|
import { Box, Text } from 'ink';
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
+
import { authenticatedFetch } from './authenticatedFetch.js';
|
|
14
15
|
import { getConfigValue } from './config.js';
|
|
15
16
|
import { API_URL } from './constants.js';
|
|
16
17
|
import { getCliVersion } from './helpers.js';
|
|
@@ -47,9 +48,7 @@ export function status() {
|
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
try {
|
|
50
|
-
const res = yield
|
|
51
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
52
|
-
});
|
|
51
|
+
const res = yield authenticatedFetch(`${API_URL}/api/cli/status`);
|
|
53
52
|
if (!res.ok) {
|
|
54
53
|
addLog(_jsx(ErrorLog, { message: "not logged in. Run `mint login` to authenticate." }));
|
|
55
54
|
return;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
|
|
11
|
+
import { getRefreshToken, storeCredentials } from './keyring.js';
|
|
12
|
+
let inflightRefresh = null;
|
|
13
|
+
export function refreshAccessToken() {
|
|
14
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
+
if (inflightRefresh)
|
|
16
|
+
return inflightRefresh;
|
|
17
|
+
inflightRefresh = doRefresh().finally(() => {
|
|
18
|
+
inflightRefresh = null;
|
|
19
|
+
});
|
|
20
|
+
return inflightRefresh;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function doRefresh() {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
try {
|
|
26
|
+
const refreshToken = yield getRefreshToken();
|
|
27
|
+
if (!refreshToken)
|
|
28
|
+
return null;
|
|
29
|
+
const res = yield fetch(TOKEN_ENDPOINT, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
client_id: STYTCH_CLIENT_ID,
|
|
34
|
+
grant_type: 'refresh_token',
|
|
35
|
+
refresh_token: refreshToken,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
return null;
|
|
40
|
+
const body = (yield res.json().catch(() => null));
|
|
41
|
+
if (!(body === null || body === void 0 ? void 0 : body.access_token) || !body.refresh_token)
|
|
42
|
+
return null;
|
|
43
|
+
yield storeCredentials(body.access_token, body.refresh_token);
|
|
44
|
+
return body.access_token;
|
|
45
|
+
}
|
|
46
|
+
catch (_a) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|