@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.
@@ -19,11 +19,16 @@ describe('keyring', () => {
19
19
  vi.resetModules();
20
20
  });
21
21
 
22
- it('stores access and refresh tokens via keytar', async () => {
22
+ it('stores access token, refresh token, and user email via keytar', async () => {
23
23
  const { storeCredentials } = await import('../src/keyring.js');
24
- await storeCredentials('access', 'refresh');
24
+ await storeCredentials('access', 'refresh', 'dev@mintlify.com');
25
25
  expect(mockKeytar.setPassword).toHaveBeenCalledWith('mintlify', 'access_token', 'access');
26
26
  expect(mockKeytar.setPassword).toHaveBeenCalledWith('mintlify', 'refresh_token', 'refresh');
27
+ expect(mockKeytar.setPassword).toHaveBeenCalledWith(
28
+ 'mintlify',
29
+ 'user_email',
30
+ 'dev@mintlify.com'
31
+ );
27
32
  });
28
33
 
29
34
  it('retrieves the access token via keytar', async () => {
@@ -40,11 +45,29 @@ describe('keyring', () => {
40
45
  expect(mockKeytar.getPassword).toHaveBeenCalledWith('mintlify', 'refresh_token');
41
46
  });
42
47
 
43
- it('deletes both tokens via keytar', async () => {
48
+ it('retrieves the user email via keytar', async () => {
49
+ const { getUserEmail } = await import('../src/keyring.js');
50
+ const email = await getUserEmail();
51
+ expect(email).toBe('test-token');
52
+ expect(mockKeytar.getPassword).toHaveBeenCalledWith('mintlify', 'user_email');
53
+ });
54
+
55
+ it('stores the user email via keytar', async () => {
56
+ const { storeUserEmail } = await import('../src/keyring.js');
57
+ await storeUserEmail('dev@mintlify.com');
58
+ expect(mockKeytar.setPassword).toHaveBeenCalledWith(
59
+ 'mintlify',
60
+ 'user_email',
61
+ 'dev@mintlify.com'
62
+ );
63
+ });
64
+
65
+ it('deletes tokens and user email via keytar', async () => {
44
66
  const { clearCredentials } = await import('../src/keyring.js');
45
67
  await clearCredentials();
46
68
  expect(mockKeytar.deletePassword).toHaveBeenCalledWith('mintlify', 'access_token');
47
69
  expect(mockKeytar.deletePassword).toHaveBeenCalledWith('mintlify', 'refresh_token');
70
+ expect(mockKeytar.deletePassword).toHaveBeenCalledWith('mintlify', 'user_email');
48
71
  });
49
72
 
50
73
  it('throws a helpful error when keytar is not installed', async () => {
package/bin/cli.js CHANGED
@@ -83,7 +83,7 @@ export const cli = ({ packageName = 'mint' }) => {
83
83
  .option('telemetry', {
84
84
  type: 'boolean',
85
85
  alias: 't',
86
- description: 'Enable or disable anonymous usage telemetry',
86
+ description: 'Enable or disable usage telemetry',
87
87
  })
88
88
  .middleware((argv) => __awaiter(void 0, void 0, void 0, function* () {
89
89
  if (argv.telemetry !== undefined && argv._.length === 0) {
package/bin/constants.js CHANGED
@@ -7,6 +7,7 @@ export const HOME_DIR = os.homedir();
7
7
  export const CONFIG_DIR = path.join(HOME_DIR, '.config', 'mintlify');
8
8
  export const CLI_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
9
  export const TELEMETRY_ASYNC_TIMEOUT_MS = 10000;
10
+ export const TELEMETRY_IDENTITY_TIMEOUT_MS = 3000;
10
11
  export const CALLBACK_PORT = 11582;
11
12
  const IS_LOCAL_BUILD = process.env.CLI_TEST_MODE === 'true' ? true : getCliVersion() == LOCAL_LINKED_CLI_VERSION;
12
13
  export const API_URL = (_a = process.env.MINTLIFY_API_URL) !== null && _a !== void 0 ? _a : (IS_LOCAL_BUILD ? 'http://localhost:5000' : 'https://leaves.mintlify.com');
package/bin/idToken.js ADDED
@@ -0,0 +1,14 @@
1
+ export function getEmailFromIdToken(idToken) {
2
+ if (!idToken)
3
+ return undefined;
4
+ try {
5
+ const payload = idToken.split('.')[1];
6
+ if (!payload)
7
+ return undefined;
8
+ const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
9
+ return typeof claims.email === 'string' && claims.email.length > 0 ? claims.email : undefined;
10
+ }
11
+ catch (_a) {
12
+ return undefined;
13
+ }
14
+ }
package/bin/keyring.js CHANGED
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  const SERVICE = 'mintlify';
11
11
  const ACCESS_TOKEN_ACCOUNT = 'access_token';
12
12
  const REFRESH_TOKEN_ACCOUNT = 'refresh_token';
13
+ const USER_EMAIL_ACCOUNT = 'user_email';
13
14
  function getKeytar() {
14
15
  return __awaiter(this, void 0, void 0, function* () {
15
16
  var _a;
@@ -23,13 +24,22 @@ function getKeytar() {
23
24
  }
24
25
  });
25
26
  }
26
- export function storeCredentials(accessToken, refreshToken) {
27
+ export function storeCredentials(accessToken, refreshToken, userEmail) {
27
28
  return __awaiter(this, void 0, void 0, function* () {
28
29
  const keytar = yield getKeytar();
29
30
  yield Promise.all([
30
31
  keytar.setPassword(SERVICE, ACCESS_TOKEN_ACCOUNT, accessToken),
31
32
  keytar.setPassword(SERVICE, REFRESH_TOKEN_ACCOUNT, refreshToken),
32
33
  ]);
34
+ try {
35
+ if (userEmail === null) {
36
+ yield keytar.deletePassword(SERVICE, USER_EMAIL_ACCOUNT);
37
+ }
38
+ else if (userEmail !== undefined) {
39
+ yield keytar.setPassword(SERVICE, USER_EMAIL_ACCOUNT, userEmail);
40
+ }
41
+ }
42
+ catch (_a) { }
33
43
  });
34
44
  }
35
45
  export function getAccessToken() {
@@ -44,12 +54,25 @@ export function getRefreshToken() {
44
54
  return keytar.getPassword(SERVICE, REFRESH_TOKEN_ACCOUNT);
45
55
  });
46
56
  }
57
+ export function getUserEmail() {
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ const keytar = yield getKeytar();
60
+ return keytar.getPassword(SERVICE, USER_EMAIL_ACCOUNT);
61
+ });
62
+ }
63
+ export function storeUserEmail(userEmail) {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ const keytar = yield getKeytar();
66
+ yield keytar.setPassword(SERVICE, USER_EMAIL_ACCOUNT, userEmail);
67
+ });
68
+ }
47
69
  export function clearCredentials() {
48
70
  return __awaiter(this, void 0, void 0, function* () {
49
71
  const keytar = yield getKeytar();
50
72
  yield Promise.all([
51
73
  keytar.deletePassword(SERVICE, ACCESS_TOKEN_ACCOUNT),
52
74
  keytar.deletePassword(SERVICE, REFRESH_TOKEN_ACCOUNT),
75
+ keytar.deletePassword(SERVICE, USER_EMAIL_ACCOUNT).catch(() => false),
53
76
  ]);
54
77
  });
55
78
  }
package/bin/login.js CHANGED
@@ -17,12 +17,13 @@ import { calculatePKCECodeChallenge, randomNonce, randomPKCECodeVerifier, random
17
17
  import { startCallbackServer } from './callbackServer.js';
18
18
  import { setConfigValue } from './config.js';
19
19
  import { DASHBOARD_URL, STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
20
+ import { getEmailFromIdToken } from './idToken.js';
20
21
  import { storeCredentials } from './keyring.js';
21
22
  import { getCliSubdomains } from './status.js';
22
23
  import { trackLoginAttempt, trackLoginFailed, trackLoginSuccess } from './telemetry/track.js';
23
24
  export function login() {
24
25
  return __awaiter(this, void 0, void 0, function* () {
25
- var _a, _b;
26
+ var _a, _b, _c;
26
27
  const codeVerifier = randomPKCECodeVerifier();
27
28
  const codeChallenge = yield calculatePKCECodeChallenge(codeVerifier);
28
29
  const nonce = randomNonce();
@@ -52,7 +53,7 @@ export function login() {
52
53
  try {
53
54
  code = yield Promise.race([codePromise, inputPromise]);
54
55
  }
55
- catch (_c) {
56
+ catch (_d) {
56
57
  closeServer();
57
58
  inputPromise.cancel();
58
59
  addLog(_jsx(ErrorLog, { message: "login cancelled" }));
@@ -79,7 +80,7 @@ export function login() {
79
80
  return;
80
81
  }
81
82
  const token = body;
82
- yield storeCredentials(token.access_token, token.refresh_token);
83
+ yield storeCredentials(token.access_token, token.refresh_token, (_c = getEmailFromIdToken(token.id_token)) !== null && _c !== void 0 ? _c : null);
83
84
  void trackLoginSuccess();
84
85
  addLog(_jsx(SuccessLog, { message: "logged in successfully" }));
85
86
  yield promptSubdomainSelection(token.access_token);
package/bin/signup.js CHANGED
@@ -16,13 +16,14 @@ import { startCallbackServer } from './callbackServer.js';
16
16
  import { clearConfigValue } from './config.js';
17
17
  import { API_URL, DASHBOARD_URL, STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
18
18
  import { isAI } from './helpers.js';
19
+ import { getEmailFromIdToken } from './idToken.js';
19
20
  import { storeCredentials } from './keyring.js';
20
21
  const sendAIUsageMessage = () => {
21
22
  addLogs(_jsx(Text, { children: "Agent Detected - sending AI friendly prompt" }), _jsx(Text, { children: '<mint-cli-instructions>' }), _jsx(Text, { children: "You were asked to create a Mintlify account. Here is how to do that with the `mint signup` command. Fill in any values you already have from this conversation; ask the user directly for anything you don't \u2014 never invent a person's name, company, or email." }), _jsx(Text, { children: "Command: `mint signup --firstName [firstName] --lastName [lastName] --company [company] --email [email]`." }), _jsx(Text, { children: "Important: this command blocks until the user clicks a verification link sent to their email, which can take several minutes. Run it as a background task, not in the foreground \u2014 do not wait on it synchronously. You'll be notified when it finishes; then tell the user their account is ready." }), _jsx(Text, { children: '</mint-cli-instructions>' }));
22
23
  };
23
24
  export function signup() {
24
25
  return __awaiter(this, arguments, void 0, function* (options = {}) {
25
- var _a, _b, _c, _d, _e, _f, _g;
26
+ var _a, _b, _c, _d, _e, _f, _g, _h;
26
27
  if (isAI() && (!options.firstName || !options.lastName || !options.company || !options.email)) {
27
28
  sendAIUsageMessage();
28
29
  return;
@@ -51,7 +52,7 @@ export function signup() {
51
52
  return;
52
53
  }
53
54
  }
54
- catch (_h) {
55
+ catch (_j) {
55
56
  closeServer();
56
57
  addLog(_jsx(ErrorLog, { message: "signup failed: could not reach the server" }));
57
58
  return;
@@ -61,7 +62,7 @@ export function signup() {
61
62
  try {
62
63
  code = yield codePromise;
63
64
  }
64
- catch (_j) {
65
+ catch (_k) {
65
66
  addLog(_jsx(ErrorLog, { message: "signup cancelled" }));
66
67
  return;
67
68
  }
@@ -83,7 +84,7 @@ export function signup() {
83
84
  return;
84
85
  }
85
86
  const token = body;
86
- yield storeCredentials(token.access_token, token.refresh_token);
87
+ yield storeCredentials(token.access_token, token.refresh_token, (_h = getEmailFromIdToken(token.id_token)) !== null && _h !== void 0 ? _h : email);
87
88
  yield clearConfigValue('subdomain').catch(() => undefined);
88
89
  addLog(_jsx(SuccessLog, { message: "account created and logged in successfully" }));
89
90
  addLog(_jsx(Text, { children: "Continue in the browser tab where you verified your email to connect GitHub." }));
package/bin/status.js CHANGED
@@ -42,11 +42,11 @@ export function getCliStatus(accessToken) {
42
42
  * Like getCliStatus, but refreshes an expired session and says why it failed, so commands can
43
43
  * tell "log in again" apart from "the server is down" and from "not allowed".
44
44
  */
45
- export function fetchCliStatus() {
45
+ export function fetchCliStatus(signal) {
46
46
  return __awaiter(this, void 0, void 0, function* () {
47
47
  let res;
48
48
  try {
49
- res = yield authenticatedFetch(`${API_URL}/api/cli/status`);
49
+ res = yield authenticatedFetch(`${API_URL}/api/cli/status`, { signal });
50
50
  }
51
51
  catch (error) {
52
52
  const detail = error instanceof Error ? error.message : String(error);
@@ -12,6 +12,7 @@ const TELEMETRY_ASYNC_TIMEOUT_MS = 10000;
12
12
  const POSTHOG_API_KEY = 'phc_eNuN6Ojnk9O7uWfC17z12AK85fNR0BY6IiGVy0Gfwzw';
13
13
  const POSTHOG_HOST = 'https://ph.mintlify.com';
14
14
  let client = null;
15
+ let pendingWork = Promise.resolve();
15
16
  export function getPostHogClient() {
16
17
  if (!client) {
17
18
  client = new PostHog(POSTHOG_API_KEY, {
@@ -20,8 +21,21 @@ export function getPostHogClient() {
20
21
  }
21
22
  return client;
22
23
  }
24
+ export function deferShutdownUntil(work) {
25
+ pendingWork = Promise.allSettled([pendingWork, work]).then(() => undefined);
26
+ }
23
27
  export function shutdownPostHog() {
24
28
  return __awaiter(this, void 0, void 0, function* () {
29
+ try {
30
+ yield Promise.race([
31
+ pendingWork,
32
+ new Promise((resolve) => {
33
+ const t = setTimeout(resolve, TELEMETRY_ASYNC_TIMEOUT_MS);
34
+ t.unref();
35
+ }),
36
+ ]);
37
+ }
38
+ catch (_a) { }
25
39
  if (!client)
26
40
  return;
27
41
  try {
@@ -33,7 +47,7 @@ export function shutdownPostHog() {
33
47
  }),
34
48
  ]);
35
49
  }
36
- catch (_a) { }
50
+ catch (_b) { }
37
51
  client = null;
38
52
  });
39
53
  }
@@ -9,9 +9,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import os from 'os';
11
11
  import { isTelemetryEnabled } from '../config.js';
12
- import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../constants.js';
12
+ import { TELEMETRY_ASYNC_TIMEOUT_MS, TELEMETRY_IDENTITY_TIMEOUT_MS } from '../constants.js';
13
13
  import { getVersions, isAI } from '../helpers.js';
14
- import { getPostHogClient } from './client.js';
14
+ import { getAccessToken, getUserEmail, storeUserEmail } from '../keyring.js';
15
+ import { fetchCliStatus } from '../status.js';
16
+ import { deferShutdownUntil, getPostHogClient } from './client.js';
15
17
  import { getDistinctId } from './distinctId.js';
16
18
  function captureWithTimeout(event, properties) {
17
19
  return __awaiter(this, void 0, void 0, function* () {
@@ -30,22 +32,39 @@ function captureWithTimeout(event, properties) {
30
32
  ]);
31
33
  });
32
34
  }
35
+ function getAuthenticatedUserEmail() {
36
+ return __awaiter(this, void 0, void 0, function* () {
37
+ try {
38
+ const [accessToken, storedEmail] = yield Promise.all([getAccessToken(), getUserEmail()]);
39
+ if (!accessToken)
40
+ return null;
41
+ if (storedEmail)
42
+ return storedEmail;
43
+ const status = yield fetchCliStatus(AbortSignal.timeout(TELEMETRY_IDENTITY_TIMEOUT_MS));
44
+ if (!status.ok)
45
+ return null;
46
+ const email = status.status.user.email;
47
+ yield storeUserEmail(email).catch(() => { });
48
+ return email;
49
+ }
50
+ catch (_a) {
51
+ return null;
52
+ }
53
+ });
54
+ }
33
55
  export function trackCommand(_a) {
34
56
  return __awaiter(this, arguments, void 0, function* ({ command, cliVersion, subdomain, }) {
35
57
  if (!isTelemetryEnabled())
36
58
  return;
37
- try {
38
- yield captureWithTimeout('cli.command.executed', {
39
- command,
40
- cli_version: cliVersion,
41
- os: os.platform(),
42
- arch: os.arch(),
43
- node_version: process.version,
44
- is_ai_agent: isAI(),
45
- subdomain,
46
- });
47
- }
48
- catch (_b) { }
59
+ const work = (() => __awaiter(this, void 0, void 0, function* () {
60
+ try {
61
+ const email = yield getAuthenticatedUserEmail();
62
+ yield captureWithTimeout('cli.command.executed', Object.assign({ command, cli_version: cliVersion, os: os.platform(), arch: os.arch(), node_version: process.version, is_ai_agent: isAI(), subdomain }, (email ? { email, $set: { email } } : {})));
63
+ }
64
+ catch (_a) { }
65
+ }))();
66
+ deferShutdownUntil(work);
67
+ yield work;
49
68
  });
50
69
  }
51
70
  function trackLoginEvent(event, extra) {
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { STYTCH_CLIENT_ID, TOKEN_ENDPOINT } from './constants.js';
11
+ import { getEmailFromIdToken } from './idToken.js';
11
12
  import { getRefreshToken, storeCredentials } from './keyring.js';
12
13
  let inflightRefresh = null;
13
14
  export function refreshAccessToken() {
@@ -40,7 +41,7 @@ function doRefresh() {
40
41
  const body = (yield res.json().catch(() => null));
41
42
  if (!(body === null || body === void 0 ? void 0 : body.access_token) || !body.refresh_token)
42
43
  return null;
43
- yield storeCredentials(body.access_token, body.refresh_token);
44
+ yield storeCredentials(body.access_token, body.refresh_token, getEmailFromIdToken(body.id_token));
44
45
  return body.access_token;
45
46
  }
46
47
  catch (_a) {