@mintlify/cli 4.0.1097 → 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/login.js +5 -5
- package/bin/logout.js +2 -3
- package/bin/status.js +15 -11
- package/bin/tokenRefresh.js +50 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/analytics/client.ts +3 -20
- package/src/authenticatedFetch.ts +42 -0
- package/src/keyring.ts +5 -0
- package/src/login.tsx +5 -5
- package/src/logout.tsx +2 -3
- package/src/status.tsx +43 -14
- package/src/tokenRefresh.ts +48 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1099",
|
|
4
4
|
"description": "The Mintlify CLI",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@inquirer/prompts": "7.9.0",
|
|
48
|
-
"@mintlify/common": "1.0.
|
|
49
|
-
"@mintlify/link-rot": "3.0.
|
|
50
|
-
"@mintlify/prebuild": "1.0.
|
|
51
|
-
"@mintlify/previewing": "4.0.
|
|
52
|
-
"@mintlify/validation": "0.1.
|
|
48
|
+
"@mintlify/common": "1.0.841",
|
|
49
|
+
"@mintlify/link-rot": "3.0.1016",
|
|
50
|
+
"@mintlify/prebuild": "1.0.983",
|
|
51
|
+
"@mintlify/previewing": "4.0.1044",
|
|
52
|
+
"@mintlify/validation": "0.1.657",
|
|
53
53
|
"adm-zip": "0.5.16",
|
|
54
54
|
"chalk": "5.2.0",
|
|
55
55
|
"color": "4.2.3",
|
|
@@ -93,5 +93,5 @@
|
|
|
93
93
|
"vitest": "2.1.9",
|
|
94
94
|
"vitest-mock-process": "1.0.4"
|
|
95
95
|
},
|
|
96
|
-
"gitHead": "
|
|
96
|
+
"gitHead": "9580be43b49210b3a2a21e11e8b6930f67801556"
|
|
97
97
|
}
|
package/src/analytics/client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authenticatedFetch } from '../authenticatedFetch.js';
|
|
1
2
|
import { API_URL } from '../constants.js';
|
|
2
3
|
import type {
|
|
3
4
|
BucketThreadsResponse,
|
|
@@ -13,32 +14,14 @@ import type {
|
|
|
13
14
|
|
|
14
15
|
type Params = Record<string, string | number | undefined>;
|
|
15
16
|
|
|
16
|
-
async function getAuthHeaders(): Promise<Record<string, string>> {
|
|
17
|
-
try {
|
|
18
|
-
const { getAccessToken } = await import('../keyring.js');
|
|
19
|
-
const token = await getAccessToken();
|
|
20
|
-
if (token) {
|
|
21
|
-
return { Authorization: `Bearer ${token}` };
|
|
22
|
-
}
|
|
23
|
-
} catch {}
|
|
24
|
-
|
|
25
|
-
const envToken = process.env.MINTLIFY_SESSION_TOKEN;
|
|
26
|
-
if (envToken) {
|
|
27
|
-
return { Authorization: `Bearer ${envToken}` };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
throw new Error('Not authenticated. Run `mint login` to authenticate.');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
17
|
async function request<T>(path: string, params: Params = {}): Promise<T> {
|
|
34
18
|
const url = new URL(`${API_URL}/api/cli/analytics${path}`);
|
|
35
19
|
for (const [key, value] of Object.entries(params)) {
|
|
36
20
|
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
37
21
|
}
|
|
38
22
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
headers: { ...authHeaders, Accept: 'application/json' },
|
|
23
|
+
const res = await authenticatedFetch(url.toString(), {
|
|
24
|
+
headers: { Accept: 'application/json' },
|
|
42
25
|
});
|
|
43
26
|
|
|
44
27
|
if (!res.ok) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getAccessToken } from './keyring.js';
|
|
2
|
+
import { refreshAccessToken } from './tokenRefresh.js';
|
|
3
|
+
|
|
4
|
+
async function getKeyringToken(): Promise<string | null> {
|
|
5
|
+
try {
|
|
6
|
+
return await getAccessToken();
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function authenticatedFetch(url: string | URL, init?: RequestInit): Promise<Response> {
|
|
13
|
+
const keyringToken = await getKeyringToken();
|
|
14
|
+
const envToken = process.env.MINTLIFY_SESSION_TOKEN;
|
|
15
|
+
const token = keyringToken ?? envToken;
|
|
16
|
+
|
|
17
|
+
if (!token) {
|
|
18
|
+
throw new Error('Not authenticated. Run `mint login` to authenticate.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const makeRequest = (t: string) =>
|
|
22
|
+
fetch(url, {
|
|
23
|
+
...init,
|
|
24
|
+
headers: { ...init?.headers, Authorization: `Bearer ${t}` },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const res = await makeRequest(token);
|
|
28
|
+
if (res.status !== 401) return res;
|
|
29
|
+
|
|
30
|
+
// If the keyring token expired, try refreshing it
|
|
31
|
+
if (keyringToken) {
|
|
32
|
+
const refreshed = await refreshAccessToken();
|
|
33
|
+
if (refreshed) return makeRequest(refreshed);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fall back to the env session token if it wasn't already used
|
|
37
|
+
if (envToken && token !== envToken) {
|
|
38
|
+
return makeRequest(envToken);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return res;
|
|
42
|
+
}
|
package/src/keyring.ts
CHANGED
|
@@ -27,6 +27,11 @@ export async function getAccessToken(): Promise<string | null> {
|
|
|
27
27
|
return keytar.getPassword(SERVICE, ACCESS_TOKEN_ACCOUNT);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export async function getRefreshToken(): Promise<string | null> {
|
|
31
|
+
const keytar = await getKeytar();
|
|
32
|
+
return keytar.getPassword(SERVICE, REFRESH_TOKEN_ACCOUNT);
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
export async function clearCredentials(): Promise<void> {
|
|
31
36
|
const keytar = await getKeytar();
|
|
32
37
|
await Promise.all([
|
package/src/login.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { input } from '@inquirer/prompts';
|
|
2
|
-
import { addLog } from '@mintlify/previewing';
|
|
2
|
+
import { addLog, ErrorLog, SuccessLog } from '@mintlify/previewing';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { Box, Text } from 'ink';
|
|
5
5
|
import open from 'open';
|
|
@@ -49,7 +49,7 @@ export async function login(): Promise<void> {
|
|
|
49
49
|
</Text>
|
|
50
50
|
<Box flexDirection="column" paddingLeft={3} gap={1}>
|
|
51
51
|
<Text dimColor>If your browser doesn't open automatically, copy this URL:</Text>
|
|
52
|
-
<Text
|
|
52
|
+
<Text dimColor>{url}</Text>
|
|
53
53
|
</Box>
|
|
54
54
|
</Box>
|
|
55
55
|
);
|
|
@@ -82,7 +82,7 @@ export async function login(): Promise<void> {
|
|
|
82
82
|
} catch {
|
|
83
83
|
closeServer();
|
|
84
84
|
inputPromise.cancel();
|
|
85
|
-
addLog(<
|
|
85
|
+
addLog(<ErrorLog message="login cancelled" />);
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -106,12 +106,12 @@ export async function login(): Promise<void> {
|
|
|
106
106
|
if (!res.ok) {
|
|
107
107
|
const reason = body.error_message ?? body.error ?? 'unknown error';
|
|
108
108
|
void trackLoginFailed(reason);
|
|
109
|
-
addLog(<
|
|
109
|
+
addLog(<ErrorLog message={`login failed: ${reason}`} />);
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const token = body as TokenResponse;
|
|
114
114
|
await storeCredentials(token.access_token, token.refresh_token);
|
|
115
115
|
void trackLoginSuccess();
|
|
116
|
-
addLog(<
|
|
116
|
+
addLog(<SuccessLog message="logged in successfully" />);
|
|
117
117
|
}
|
package/src/logout.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { addLog } from '@mintlify/previewing';
|
|
2
|
-
import { Text } from 'ink';
|
|
1
|
+
import { addLog, SuccessLog } from '@mintlify/previewing';
|
|
3
2
|
|
|
4
3
|
import { clearCredentials } from './keyring.js';
|
|
5
4
|
|
|
6
5
|
export async function logout(): Promise<void> {
|
|
7
6
|
await clearCredentials();
|
|
8
|
-
addLog(<
|
|
7
|
+
addLog(<SuccessLog message="logged out successfully" />);
|
|
9
8
|
}
|
package/src/status.tsx
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { addLog } from '@mintlify/previewing';
|
|
2
|
-
import { Text } from 'ink';
|
|
1
|
+
import { addLog, ErrorLog } from '@mintlify/previewing';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
+
import { authenticatedFetch } from './authenticatedFetch.js';
|
|
6
|
+
import { getConfigValue } from './config.js';
|
|
5
7
|
import { API_URL } from './constants.js';
|
|
8
|
+
import { getCliVersion } from './helpers.js';
|
|
6
9
|
import { getAccessToken } from './keyring.js';
|
|
7
10
|
|
|
8
11
|
const StatusResponseSchema = z.object({
|
|
@@ -29,17 +32,15 @@ export async function status(): Promise<void> {
|
|
|
29
32
|
const accessToken = await getAccessToken();
|
|
30
33
|
|
|
31
34
|
if (!accessToken) {
|
|
32
|
-
addLog(<
|
|
35
|
+
addLog(<ErrorLog message="not logged in. Run `mint login` to authenticate." />);
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
try {
|
|
37
|
-
const res = await
|
|
38
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
39
|
-
});
|
|
40
|
+
const res = await authenticatedFetch(`${API_URL}/api/cli/status`);
|
|
40
41
|
|
|
41
42
|
if (!res.ok) {
|
|
42
|
-
addLog(<
|
|
43
|
+
addLog(<ErrorLog message="not logged in. Run `mint login` to authenticate." />);
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -47,18 +48,46 @@ export async function status(): Promise<void> {
|
|
|
47
48
|
const parsed = StatusResponseSchema.safeParse(json);
|
|
48
49
|
|
|
49
50
|
if (!parsed.success) {
|
|
50
|
-
addLog(<
|
|
51
|
+
addLog(<ErrorLog message="unexpected response from server. please try again." />);
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
const { user, org } = parsed.data;
|
|
55
|
+
const { user, org, subdomain: apiSubdomain } = parsed.data;
|
|
56
|
+
const version = getCliVersion();
|
|
57
|
+
const subdomain = getConfigValue('subdomain') ?? apiSubdomain ?? null;
|
|
55
58
|
addLog(
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
<Box flexDirection="column" paddingY={1}>
|
|
60
|
+
{version && (
|
|
61
|
+
<Box>
|
|
62
|
+
<Box minWidth={16}>
|
|
63
|
+
<Text dimColor>Version</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
<Text>{version}</Text>
|
|
66
|
+
</Box>
|
|
67
|
+
)}
|
|
68
|
+
<Box>
|
|
69
|
+
<Box minWidth={16}>
|
|
70
|
+
<Text dimColor>Email</Text>
|
|
71
|
+
</Box>
|
|
72
|
+
<Text>{user.email}</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
<Box>
|
|
75
|
+
<Box minWidth={16}>
|
|
76
|
+
<Text dimColor>Organization</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
<Text>{org.name}</Text>
|
|
79
|
+
</Box>
|
|
80
|
+
{subdomain && (
|
|
81
|
+
<Box>
|
|
82
|
+
<Box minWidth={16}>
|
|
83
|
+
<Text dimColor>Subdomain</Text>
|
|
84
|
+
</Box>
|
|
85
|
+
<Text>{subdomain}</Text>
|
|
86
|
+
</Box>
|
|
87
|
+
)}
|
|
88
|
+
</Box>
|
|
60
89
|
);
|
|
61
90
|
} catch (e) {
|
|
62
|
-
addLog(<
|
|
91
|
+
addLog(<ErrorLog message="unexpected response from server. please try again." />);
|
|
63
92
|
}
|
|
64
93
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
|
|
2
|
+
import { getRefreshToken, storeCredentials } from './keyring.js';
|
|
3
|
+
|
|
4
|
+
interface TokenResponse {
|
|
5
|
+
access_token: string;
|
|
6
|
+
refresh_token: string;
|
|
7
|
+
token_type: 'bearer';
|
|
8
|
+
expires_in: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let inflightRefresh: Promise<string | null> | null = null;
|
|
12
|
+
|
|
13
|
+
export async function refreshAccessToken(): Promise<string | null> {
|
|
14
|
+
if (inflightRefresh) return inflightRefresh;
|
|
15
|
+
|
|
16
|
+
inflightRefresh = doRefresh().finally(() => {
|
|
17
|
+
inflightRefresh = null;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return inflightRefresh;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function doRefresh(): Promise<string | null> {
|
|
24
|
+
try {
|
|
25
|
+
const refreshToken = await getRefreshToken();
|
|
26
|
+
if (!refreshToken) return null;
|
|
27
|
+
|
|
28
|
+
const res = await fetch(TOKEN_ENDPOINT, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
client_id: STYTCH_CLIENT_ID,
|
|
33
|
+
grant_type: 'refresh_token',
|
|
34
|
+
refresh_token: refreshToken,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!res.ok) return null;
|
|
39
|
+
|
|
40
|
+
const body = (await res.json().catch(() => null)) as TokenResponse | null;
|
|
41
|
+
if (!body?.access_token || !body.refresh_token) return null;
|
|
42
|
+
|
|
43
|
+
await storeCredentials(body.access_token, body.refresh_token);
|
|
44
|
+
return body.access_token;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|