@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1472",
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.1127",
49
- "@mintlify/link-rot": "3.0.1329",
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.1281",
52
- "@mintlify/previewing": "4.0.1353",
53
- "@mintlify/validation": "0.1.844",
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.357",
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": "bb12d905a42453ef8bdcb23a928bbe9de59ea218"
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 anonymous usage telemetry',
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(accessToken: string, refreshToken: string): Promise<void> {
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(token.access_token, token.refresh_token);
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(token.access_token, token.refresh_token);
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' };
@@ -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([
@@ -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 { getPostHogClient } from './client.js';
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
- try {
42
- await captureWithTimeout('cli.command.executed', {
43
- command,
44
- cli_version: cliVersion,
45
- os: os.platform(),
46
- arch: os.arch(),
47
- node_version: process.version,
48
- is_ai_agent: isAI(),
49
- subdomain,
50
- });
51
- } catch {}
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> {
@@ -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(body.access_token, body.refresh_token);
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;