-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathclient.js
More file actions
168 lines (148 loc) · 4.51 KB
/
client.js
File metadata and controls
168 lines (148 loc) · 4.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/**
* API Client Factory
*
* Creates a configured API client for making HTTP requests to Vizzly.
* The client handles authentication, token refresh, and error handling.
*/
import { AuthError, VizzlyError } from '../errors/vizzly-error.js';
import { getAuthTokens, saveAuthTokens } from '../utils/global-config.js';
import { getPackageVersion } from '../utils/package-info.js';
import {
buildApiUrl,
buildRequestHeaders,
buildUserAgent,
extractErrorBody,
isAuthError,
parseApiError,
shouldRetryWithRefresh,
} from './core.js';
/**
* Default API URL
*/
export const DEFAULT_API_URL = 'https://app.vizzly.dev';
/**
* Create an API client with the given configuration
*
* @param {Object} options - Client options
* @param {string} options.baseUrl - Base API URL
* @param {string} options.token - API token (apiKey)
* @param {string} options.command - Command name for user agent
* @param {string} options.sdkUserAgent - Optional SDK user agent string
* @param {boolean} options.allowNoToken - Allow requests without token
* @returns {Object} API client with request method
*/
export function createApiClient(options = {}) {
let baseUrl = options.baseUrl || options.apiUrl || DEFAULT_API_URL;
let token = options.token || options.apiKey || null;
let command = options.command || 'api';
let version = getPackageVersion();
let userAgent = buildUserAgent(
version,
command,
options.sdkUserAgent || options.userAgent
);
let allowNoToken = options.allowNoToken || false;
// Validate token requirement
if (!token && !allowNoToken) {
throw new VizzlyError(
'No API token provided. Set VIZZLY_TOKEN environment variable or link a project in the TDD dashboard.'
);
}
/**
* Make an API request
*
* @param {string} endpoint - API endpoint (e.g., '/api/sdk/builds')
* @param {Object} fetchOptions - Fetch options (method, body, headers, etc.)
* @param {boolean} isRetry - Whether this is a retry after token refresh
* @returns {Promise<Object>} Parsed JSON response
*/
async function request(endpoint, fetchOptions = {}, isRetry = false) {
let url = buildApiUrl(baseUrl, endpoint);
let headers = buildRequestHeaders({
token,
userAgent,
contentType: fetchOptions.headers?.['Content-Type'],
extra: fetchOptions.headers || {},
});
let response = await fetch(url, {
...fetchOptions,
headers,
});
if (!response.ok) {
let errorBody = await extractErrorBody(response);
// Handle 401 with token refresh
if (
shouldRetryWithRefresh(
response.status,
isRetry,
await hasRefreshToken()
)
) {
let refreshed = await attemptTokenRefresh();
if (refreshed) {
token = refreshed;
return request(endpoint, fetchOptions, true);
}
}
// Auth error
if (isAuthError(response.status)) {
throw new AuthError(
'Invalid or expired API token. Link a project via "vizzly project:select" or set VIZZLY_TOKEN.'
);
}
// Other errors
let error = parseApiError(response.status, errorBody, url);
throw new VizzlyError(error.message, error.code, {
status: error.status,
});
}
return response.json();
}
/**
* Check if refresh token is available
*/
async function hasRefreshToken() {
let auth = await getAuthTokens(baseUrl);
return !!auth?.refreshToken;
}
/**
* Attempt to refresh the access token
* @returns {Promise<string|null>} New token or null if refresh failed
*/
async function attemptTokenRefresh() {
let auth = await getAuthTokens(baseUrl);
if (!auth?.refreshToken) return null;
try {
let refreshUrl = buildApiUrl(baseUrl, '/api/auth/cli/refresh');
let response = await fetch(refreshUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent,
},
body: JSON.stringify({ refreshToken: auth.refreshToken }),
});
if (!response.ok) return null;
let data = await response.json();
// Save new tokens
await saveAuthTokens(
{
accessToken: data.accessToken,
refreshToken: data.refreshToken,
expiresAt: data.expiresAt,
user: auth.user,
},
baseUrl
);
return data.accessToken;
} catch {
return null;
}
}
return {
request,
getBaseUrl: () => baseUrl,
getToken: () => token,
getUserAgent: () => userAgent,
};
}