@mintlify/cli 4.0.1472 → 4.0.1474
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__/keyring.test.ts +26 -3
- package/bin/cli.js +1 -1
- package/bin/constants.js +1 -0
- package/bin/idToken.js +14 -0
- package/bin/keyring.js +24 -1
- package/bin/login.js +4 -3
- package/bin/signup.js +5 -4
- package/bin/status.js +2 -2
- package/bin/telemetry/client.js +15 -1
- package/bin/telemetry/track.js +33 -14
- package/bin/tokenRefresh.js +2 -1
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/cli.tsx +1 -1
- package/src/constants.ts +1 -0
- package/src/idToken.ts +17 -0
- package/src/keyring.ts +25 -1
- package/src/login.tsx +6 -1
- package/src/signup.tsx +6 -1
- package/src/status.tsx +2 -2
- package/src/telemetry/client.ts +15 -0
- package/src/telemetry/track.ts +39 -13
- package/src/tokenRefresh.ts +7 -1
- package/__test__/telemetry.test.ts +0 -418
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1474",
|
|
4
4
|
"description": "The Mintlify CLI",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@inquirer/prompts": "7.9.0",
|
|
48
|
-
"@mintlify/common": "1.0.
|
|
49
|
-
"@mintlify/link-rot": "3.0.
|
|
48
|
+
"@mintlify/common": "1.0.1128",
|
|
49
|
+
"@mintlify/link-rot": "3.0.1330",
|
|
50
50
|
"@mintlify/models": "0.0.351",
|
|
51
|
-
"@mintlify/prebuild": "1.0.
|
|
52
|
-
"@mintlify/previewing": "4.0.
|
|
53
|
-
"@mintlify/validation": "0.1.
|
|
51
|
+
"@mintlify/prebuild": "1.0.1282",
|
|
52
|
+
"@mintlify/previewing": "4.0.1354",
|
|
53
|
+
"@mintlify/validation": "0.1.845",
|
|
54
54
|
"adm-zip": "0.6.0",
|
|
55
55
|
"chalk": "5.2.0",
|
|
56
56
|
"color": "4.2.3",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"keytar": "7.9.0"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@mintlify/editor": "0.0.
|
|
89
|
+
"@mintlify/editor": "0.0.358",
|
|
90
90
|
"@mintlify/ts-config": "2.0.2",
|
|
91
91
|
"@tsconfig/recommended": "1.0.2",
|
|
92
92
|
"@types/detect-port": "1.3.2",
|
|
@@ -105,5 +105,5 @@
|
|
|
105
105
|
"vitest": "2.1.9",
|
|
106
106
|
"vitest-mock-process": "1.0.4"
|
|
107
107
|
},
|
|
108
|
-
"gitHead": "
|
|
108
|
+
"gitHead": "5b773bbb5a32822d5875d5e350e48eb374065be8"
|
|
109
109
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -102,7 +102,7 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
102
102
|
.option('telemetry', {
|
|
103
103
|
type: 'boolean',
|
|
104
104
|
alias: 't',
|
|
105
|
-
description: 'Enable or disable
|
|
105
|
+
description: 'Enable or disable usage telemetry',
|
|
106
106
|
})
|
|
107
107
|
.middleware(async (argv) => {
|
|
108
108
|
if (argv.telemetry !== undefined && argv._.length === 0) {
|
package/src/constants.ts
CHANGED
|
@@ -9,6 +9,7 @@ export const HOME_DIR = os.homedir();
|
|
|
9
9
|
export const CONFIG_DIR = path.join(HOME_DIR, '.config', 'mintlify');
|
|
10
10
|
export const CLI_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
11
11
|
export const TELEMETRY_ASYNC_TIMEOUT_MS = 10_000;
|
|
12
|
+
export const TELEMETRY_IDENTITY_TIMEOUT_MS = 3_000;
|
|
12
13
|
|
|
13
14
|
export const CALLBACK_PORT = 11582;
|
|
14
15
|
|
package/src/idToken.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface IdTokenClaims {
|
|
2
|
+
email?: unknown;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getEmailFromIdToken(idToken: string | undefined): string | undefined {
|
|
6
|
+
if (!idToken) return undefined;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const payload = idToken.split('.')[1];
|
|
10
|
+
if (!payload) return undefined;
|
|
11
|
+
|
|
12
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as IdTokenClaims;
|
|
13
|
+
return typeof claims.email === 'string' && claims.email.length > 0 ? claims.email : undefined;
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/keyring.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const SERVICE = 'mintlify';
|
|
2
2
|
const ACCESS_TOKEN_ACCOUNT = 'access_token';
|
|
3
3
|
const REFRESH_TOKEN_ACCOUNT = 'refresh_token';
|
|
4
|
+
const USER_EMAIL_ACCOUNT = 'user_email';
|
|
4
5
|
|
|
5
6
|
async function getKeytar(): Promise<typeof import('keytar')> {
|
|
6
7
|
try {
|
|
@@ -14,12 +15,24 @@ async function getKeytar(): Promise<typeof import('keytar')> {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export async function storeCredentials(
|
|
18
|
+
export async function storeCredentials(
|
|
19
|
+
accessToken: string,
|
|
20
|
+
refreshToken: string,
|
|
21
|
+
userEmail?: string | null
|
|
22
|
+
): Promise<void> {
|
|
18
23
|
const keytar = await getKeytar();
|
|
19
24
|
await Promise.all([
|
|
20
25
|
keytar.setPassword(SERVICE, ACCESS_TOKEN_ACCOUNT, accessToken),
|
|
21
26
|
keytar.setPassword(SERVICE, REFRESH_TOKEN_ACCOUNT, refreshToken),
|
|
22
27
|
]);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
if (userEmail === null) {
|
|
31
|
+
await keytar.deletePassword(SERVICE, USER_EMAIL_ACCOUNT);
|
|
32
|
+
} else if (userEmail !== undefined) {
|
|
33
|
+
await keytar.setPassword(SERVICE, USER_EMAIL_ACCOUNT, userEmail);
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
export async function getAccessToken(): Promise<string | null> {
|
|
@@ -32,10 +45,21 @@ export async function getRefreshToken(): Promise<string | null> {
|
|
|
32
45
|
return keytar.getPassword(SERVICE, REFRESH_TOKEN_ACCOUNT);
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
export async function getUserEmail(): Promise<string | null> {
|
|
49
|
+
const keytar = await getKeytar();
|
|
50
|
+
return keytar.getPassword(SERVICE, USER_EMAIL_ACCOUNT);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function storeUserEmail(userEmail: string): Promise<void> {
|
|
54
|
+
const keytar = await getKeytar();
|
|
55
|
+
await keytar.setPassword(SERVICE, USER_EMAIL_ACCOUNT, userEmail);
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
export async function clearCredentials(): Promise<void> {
|
|
36
59
|
const keytar = await getKeytar();
|
|
37
60
|
await Promise.all([
|
|
38
61
|
keytar.deletePassword(SERVICE, ACCESS_TOKEN_ACCOUNT),
|
|
39
62
|
keytar.deletePassword(SERVICE, REFRESH_TOKEN_ACCOUNT),
|
|
63
|
+
keytar.deletePassword(SERVICE, USER_EMAIL_ACCOUNT).catch(() => false),
|
|
40
64
|
]);
|
|
41
65
|
}
|
package/src/login.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { startCallbackServer } from './callbackServer.js';
|
|
14
14
|
import { setConfigValue } from './config.js';
|
|
15
15
|
import { DASHBOARD_URL, STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
|
|
16
|
+
import { getEmailFromIdToken } from './idToken.js';
|
|
16
17
|
import { storeCredentials } from './keyring.js';
|
|
17
18
|
import { getCliSubdomains } from './status.js';
|
|
18
19
|
import { trackLoginAttempt, trackLoginFailed, trackLoginSuccess } from './telemetry/track.js';
|
|
@@ -113,7 +114,11 @@ export async function login(): Promise<void> {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
const token = body as TokenResponse;
|
|
116
|
-
await storeCredentials(
|
|
117
|
+
await storeCredentials(
|
|
118
|
+
token.access_token,
|
|
119
|
+
token.refresh_token,
|
|
120
|
+
getEmailFromIdToken(token.id_token) ?? null
|
|
121
|
+
);
|
|
117
122
|
void trackLoginSuccess();
|
|
118
123
|
addLog(<SuccessLog message="logged in successfully" />);
|
|
119
124
|
await promptSubdomainSelection(token.access_token);
|
package/src/signup.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { startCallbackServer } from './callbackServer.js';
|
|
|
12
12
|
import { clearConfigValue } from './config.js';
|
|
13
13
|
import { API_URL, DASHBOARD_URL, STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
|
|
14
14
|
import { isAI } from './helpers.js';
|
|
15
|
+
import { getEmailFromIdToken } from './idToken.js';
|
|
15
16
|
import { storeCredentials } from './keyring.js';
|
|
16
17
|
import type { TokenResponse } from './login.js';
|
|
17
18
|
|
|
@@ -124,7 +125,11 @@ export async function signup(options: SignupOptions = {}): Promise<void> {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
const token = body as TokenResponse;
|
|
127
|
-
await storeCredentials(
|
|
128
|
+
await storeCredentials(
|
|
129
|
+
token.access_token,
|
|
130
|
+
token.refresh_token,
|
|
131
|
+
getEmailFromIdToken(token.id_token) ?? email
|
|
132
|
+
);
|
|
128
133
|
await clearConfigValue('subdomain').catch(() => undefined);
|
|
129
134
|
addLog(<SuccessLog message="account created and logged in successfully" />);
|
|
130
135
|
addLog(<Text>Continue in the browser tab where you verified your email to connect GitHub.</Text>);
|
package/src/status.tsx
CHANGED
|
@@ -38,10 +38,10 @@ export type CliStatusResult =
|
|
|
38
38
|
* Like getCliStatus, but refreshes an expired session and says why it failed, so commands can
|
|
39
39
|
* tell "log in again" apart from "the server is down" and from "not allowed".
|
|
40
40
|
*/
|
|
41
|
-
export async function fetchCliStatus(): Promise<CliStatusResult> {
|
|
41
|
+
export async function fetchCliStatus(signal?: AbortSignal): Promise<CliStatusResult> {
|
|
42
42
|
let res: Response;
|
|
43
43
|
try {
|
|
44
|
-
res = await authenticatedFetch(`${API_URL}/api/cli/status
|
|
44
|
+
res = await authenticatedFetch(`${API_URL}/api/cli/status`, { signal });
|
|
45
45
|
} catch (error) {
|
|
46
46
|
const detail = error instanceof Error ? error.message : String(error);
|
|
47
47
|
if (/not authenticated/i.test(detail)) return { ok: false, reason: 'unauthenticated' };
|
package/src/telemetry/client.ts
CHANGED
|
@@ -6,6 +6,7 @@ const POSTHOG_API_KEY = 'phc_eNuN6Ojnk9O7uWfC17z12AK85fNR0BY6IiGVy0Gfwzw';
|
|
|
6
6
|
const POSTHOG_HOST = 'https://ph.mintlify.com';
|
|
7
7
|
|
|
8
8
|
let client: PostHog | null = null;
|
|
9
|
+
let pendingWork: Promise<void> = Promise.resolve();
|
|
9
10
|
|
|
10
11
|
export function getPostHogClient(): PostHog {
|
|
11
12
|
if (!client) {
|
|
@@ -16,7 +17,21 @@ export function getPostHogClient(): PostHog {
|
|
|
16
17
|
return client;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export function deferShutdownUntil(work: Promise<void>): void {
|
|
21
|
+
pendingWork = Promise.allSettled([pendingWork, work]).then(() => undefined);
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export async function shutdownPostHog(): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
await Promise.race([
|
|
27
|
+
pendingWork,
|
|
28
|
+
new Promise<void>((resolve) => {
|
|
29
|
+
const t = setTimeout(resolve, TELEMETRY_ASYNC_TIMEOUT_MS);
|
|
30
|
+
t.unref();
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
20
35
|
if (!client) return;
|
|
21
36
|
try {
|
|
22
37
|
await Promise.race([
|
package/src/telemetry/track.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
|
|
3
3
|
import { isTelemetryEnabled } from '../config.js';
|
|
4
|
-
import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../constants.js';
|
|
4
|
+
import { TELEMETRY_ASYNC_TIMEOUT_MS, TELEMETRY_IDENTITY_TIMEOUT_MS } from '../constants.js';
|
|
5
5
|
import { getVersions, isAI } from '../helpers.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getAccessToken, getUserEmail, storeUserEmail } from '../keyring.js';
|
|
7
|
+
import { fetchCliStatus } from '../status.js';
|
|
8
|
+
import { deferShutdownUntil, getPostHogClient } from './client.js';
|
|
7
9
|
import { getDistinctId } from './distinctId.js';
|
|
8
10
|
|
|
9
11
|
export interface TrackCommandOptions {
|
|
@@ -31,6 +33,23 @@ async function captureWithTimeout(
|
|
|
31
33
|
]);
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
async function getAuthenticatedUserEmail(): Promise<string | null> {
|
|
37
|
+
try {
|
|
38
|
+
const [accessToken, storedEmail] = await Promise.all([getAccessToken(), getUserEmail()]);
|
|
39
|
+
if (!accessToken) return null;
|
|
40
|
+
if (storedEmail) return storedEmail;
|
|
41
|
+
|
|
42
|
+
const status = await fetchCliStatus(AbortSignal.timeout(TELEMETRY_IDENTITY_TIMEOUT_MS));
|
|
43
|
+
if (!status.ok) return null;
|
|
44
|
+
|
|
45
|
+
const email = status.status.user.email;
|
|
46
|
+
await storeUserEmail(email).catch(() => {});
|
|
47
|
+
return email;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
export async function trackCommand({
|
|
35
54
|
command,
|
|
36
55
|
cliVersion,
|
|
@@ -38,17 +57,24 @@ export async function trackCommand({
|
|
|
38
57
|
}: TrackCommandOptions): Promise<void> {
|
|
39
58
|
if (!isTelemetryEnabled()) return;
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
const work = (async () => {
|
|
61
|
+
try {
|
|
62
|
+
const email = await getAuthenticatedUserEmail();
|
|
63
|
+
await captureWithTimeout('cli.command.executed', {
|
|
64
|
+
command,
|
|
65
|
+
cli_version: cliVersion,
|
|
66
|
+
os: os.platform(),
|
|
67
|
+
arch: os.arch(),
|
|
68
|
+
node_version: process.version,
|
|
69
|
+
is_ai_agent: isAI(),
|
|
70
|
+
subdomain,
|
|
71
|
+
...(email ? { email, $set: { email } } : {}),
|
|
72
|
+
});
|
|
73
|
+
} catch {}
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
deferShutdownUntil(work);
|
|
77
|
+
await work;
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
async function trackLoginEvent(event: string, extra?: Record<string, unknown>): Promise<void> {
|
package/src/tokenRefresh.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
|
|
2
|
+
import { getEmailFromIdToken } from './idToken.js';
|
|
2
3
|
import { getRefreshToken, storeCredentials } from './keyring.js';
|
|
3
4
|
|
|
4
5
|
interface TokenResponse {
|
|
5
6
|
access_token: string;
|
|
7
|
+
id_token?: string;
|
|
6
8
|
refresh_token: string;
|
|
7
9
|
token_type: 'bearer';
|
|
8
10
|
expires_in: number;
|
|
@@ -40,7 +42,11 @@ async function doRefresh(): Promise<string | null> {
|
|
|
40
42
|
const body = (await res.json().catch(() => null)) as TokenResponse | null;
|
|
41
43
|
if (!body?.access_token || !body.refresh_token) return null;
|
|
42
44
|
|
|
43
|
-
await storeCredentials(
|
|
45
|
+
await storeCredentials(
|
|
46
|
+
body.access_token,
|
|
47
|
+
body.refresh_token,
|
|
48
|
+
getEmailFromIdToken(body.id_token)
|
|
49
|
+
);
|
|
44
50
|
return body.access_token;
|
|
45
51
|
} catch {
|
|
46
52
|
return null;
|