@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1098",
3
+ "version": "4.0.1099",
4
4
  "description": "The Mintlify CLI",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -93,5 +93,5 @@
93
93
  "vitest": "2.1.9",
94
94
  "vitest-mock-process": "1.0.4"
95
95
  },
96
- "gitHead": "edd192ddf9723fff5a7d818a9e6937e17b8806a1"
96
+ "gitHead": "9580be43b49210b3a2a21e11e8b6930f67801556"
97
97
  }
@@ -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 authHeaders = await getAuthHeaders();
40
- const res = await fetch(url, {
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/status.tsx CHANGED
@@ -2,6 +2,7 @@ import { addLog, ErrorLog } from '@mintlify/previewing';
2
2
  import { Box, Text } from 'ink';
3
3
  import { z } from 'zod';
4
4
 
5
+ import { authenticatedFetch } from './authenticatedFetch.js';
5
6
  import { getConfigValue } from './config.js';
6
7
  import { API_URL } from './constants.js';
7
8
  import { getCliVersion } from './helpers.js';
@@ -36,9 +37,7 @@ export async function status(): Promise<void> {
36
37
  }
37
38
 
38
39
  try {
39
- const res = await fetch(`${API_URL}/api/cli/status`, {
40
- headers: { Authorization: `Bearer ${accessToken}` },
41
- });
40
+ const res = await authenticatedFetch(`${API_URL}/api/cli/status`);
42
41
 
43
42
  if (!res.ok) {
44
43
  addLog(<ErrorLog message="not logged in. Run `mint login` to authenticate." />);
@@ -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
+ }