@enactprotocol/api 2.0.0
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 +34 -0
- package/src/attestations.ts +461 -0
- package/src/auth.ts +293 -0
- package/src/client.ts +349 -0
- package/src/download.ts +298 -0
- package/src/index.ts +109 -0
- package/src/publish.ts +316 -0
- package/src/search.ts +147 -0
- package/src/trust.ts +203 -0
- package/src/types.ts +468 -0
- package/src/utils.ts +86 -0
package/src/auth.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication functionality (v2)
|
|
3
|
+
* Handles OAuth-based authentication for the Enact registry
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EnactApiClient } from "./client";
|
|
7
|
+
import type {
|
|
8
|
+
CurrentUser,
|
|
9
|
+
OAuthCallbackRequest,
|
|
10
|
+
OAuthLoginRequest,
|
|
11
|
+
OAuthLoginResponse,
|
|
12
|
+
OAuthProvider,
|
|
13
|
+
OAuthTokenResponse,
|
|
14
|
+
RefreshTokenRequest,
|
|
15
|
+
RefreshTokenResponse,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Authentication result
|
|
20
|
+
*/
|
|
21
|
+
export interface AuthResult {
|
|
22
|
+
/** Whether authentication succeeded */
|
|
23
|
+
success: boolean;
|
|
24
|
+
/** Authentication token (if successful) */
|
|
25
|
+
token?: string | undefined;
|
|
26
|
+
/** Current user info (if successful) */
|
|
27
|
+
user?: AuthUser | undefined;
|
|
28
|
+
/** Error message (if failed) */
|
|
29
|
+
error?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authenticated user info
|
|
34
|
+
*/
|
|
35
|
+
export interface AuthUser {
|
|
36
|
+
/** Username */
|
|
37
|
+
username: string;
|
|
38
|
+
/** Email address */
|
|
39
|
+
email: string;
|
|
40
|
+
/** Namespaces owned */
|
|
41
|
+
namespaces: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Authentication status
|
|
46
|
+
*/
|
|
47
|
+
export interface AuthStatus {
|
|
48
|
+
/** Whether currently authenticated */
|
|
49
|
+
authenticated: boolean;
|
|
50
|
+
/** Current user (if authenticated) */
|
|
51
|
+
user?: AuthUser | undefined;
|
|
52
|
+
/** Token expiration time (if available) */
|
|
53
|
+
expiresAt?: Date | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initiate OAuth login (v2)
|
|
58
|
+
*
|
|
59
|
+
* @param client - API client instance
|
|
60
|
+
* @param provider - OAuth provider (github, google, microsoft)
|
|
61
|
+
* @param redirectUri - Callback URL (usually http://localhost:PORT/callback)
|
|
62
|
+
* @returns Authorization URL to redirect user to
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const result = await initiateLogin(client, "github", "http://localhost:9876/callback");
|
|
67
|
+
* console.log(`Visit: ${result.authUrl}`);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export async function initiateLogin(
|
|
71
|
+
client: EnactApiClient,
|
|
72
|
+
provider: OAuthProvider,
|
|
73
|
+
redirectUri: string
|
|
74
|
+
): Promise<OAuthLoginResponse> {
|
|
75
|
+
const response = await client.post<OAuthLoginResponse>("/auth/login", {
|
|
76
|
+
provider,
|
|
77
|
+
redirect_uri: redirectUri,
|
|
78
|
+
} as OAuthLoginRequest);
|
|
79
|
+
|
|
80
|
+
return response.data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Exchange OAuth code for tokens (v2)
|
|
85
|
+
*
|
|
86
|
+
* @param client - API client instance
|
|
87
|
+
* @param provider - OAuth provider used
|
|
88
|
+
* @param code - Authorization code from OAuth callback
|
|
89
|
+
* @returns Token response with access token, refresh token, and user info
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* const tokens = await exchangeCodeForToken(client, "github", "auth_code_123");
|
|
94
|
+
* client.setAuthToken(tokens.access_token);
|
|
95
|
+
* console.log(`Logged in as ${tokens.user.username}`);
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export async function exchangeCodeForToken(
|
|
99
|
+
client: EnactApiClient,
|
|
100
|
+
provider: OAuthProvider,
|
|
101
|
+
code: string
|
|
102
|
+
): Promise<OAuthTokenResponse> {
|
|
103
|
+
const response = await client.post<OAuthTokenResponse>("/auth/callback", {
|
|
104
|
+
provider,
|
|
105
|
+
code,
|
|
106
|
+
} as OAuthCallbackRequest);
|
|
107
|
+
|
|
108
|
+
return response.data;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Refresh an expired access token (v2)
|
|
113
|
+
*
|
|
114
|
+
* @param client - API client instance
|
|
115
|
+
* @param refreshToken - Refresh token obtained during login
|
|
116
|
+
* @returns New access token and expiration
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const newToken = await refreshAccessToken(client, storedRefreshToken);
|
|
121
|
+
* client.setAuthToken(newToken.access_token);
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export async function refreshAccessToken(
|
|
125
|
+
client: EnactApiClient,
|
|
126
|
+
refreshToken: string
|
|
127
|
+
): Promise<RefreshTokenResponse> {
|
|
128
|
+
const response = await client.post<RefreshTokenResponse>("/auth/refresh", {
|
|
129
|
+
refresh_token: refreshToken,
|
|
130
|
+
} as RefreshTokenRequest);
|
|
131
|
+
|
|
132
|
+
return response.data;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Authenticate with the Enact registry (v2 OAuth flow)
|
|
137
|
+
*
|
|
138
|
+
* This is a convenience wrapper that initiates an OAuth flow:
|
|
139
|
+
* 1. Opens a browser for authentication
|
|
140
|
+
* 2. User logs in via their provider (GitHub, Google, etc.)
|
|
141
|
+
* 3. Receives a token from the registry
|
|
142
|
+
*
|
|
143
|
+
* Note: The actual OAuth callback handling requires a local HTTP server,
|
|
144
|
+
* which should be implemented in the CLI package.
|
|
145
|
+
*
|
|
146
|
+
* @param client - API client instance
|
|
147
|
+
* @returns Authentication result
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* const result = await authenticate(client);
|
|
152
|
+
* if (result.success) {
|
|
153
|
+
* client.setAuthToken(result.token);
|
|
154
|
+
* console.log(`Logged in as ${result.user.username}`);
|
|
155
|
+
* }
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export async function authenticate(_client: EnactApiClient): Promise<AuthResult> {
|
|
159
|
+
// This is a placeholder for the full OAuth flow
|
|
160
|
+
// The actual implementation should be in the CLI package
|
|
161
|
+
// which can start a local server and open a browser
|
|
162
|
+
//
|
|
163
|
+
// Typical flow:
|
|
164
|
+
// 1. const loginResponse = await initiateLogin(client, "github", redirectUri);
|
|
165
|
+
// 2. Open browser to loginResponse.auth_url
|
|
166
|
+
// 3. Start local server on redirectUri to receive callback
|
|
167
|
+
// 4. Extract code from callback
|
|
168
|
+
// 5. const tokens = await exchangeCodeForToken(client, "github", code);
|
|
169
|
+
// 6. Return { success: true, token: tokens.access_token, user: {...} }
|
|
170
|
+
|
|
171
|
+
throw new Error(
|
|
172
|
+
"authenticate() must be implemented in the CLI package. " +
|
|
173
|
+
"Use initiateLogin() and exchangeCodeForToken() for OAuth flow."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Log out by clearing the authentication token
|
|
179
|
+
*
|
|
180
|
+
* @param client - API client instance
|
|
181
|
+
*/
|
|
182
|
+
export function logout(client: EnactApiClient): void {
|
|
183
|
+
client.setAuthToken(undefined);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get current user info (v2)
|
|
188
|
+
*
|
|
189
|
+
* @param client - API client instance (must be authenticated)
|
|
190
|
+
* @returns Current user info
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* const user = await getCurrentUser(client);
|
|
195
|
+
* console.log(`Logged in as ${user.username}`);
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export async function getCurrentUser(client: EnactApiClient): Promise<CurrentUser> {
|
|
199
|
+
const response = await client.get<CurrentUser>("/auth/me");
|
|
200
|
+
return response.data;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get current authentication status (v2)
|
|
205
|
+
*
|
|
206
|
+
* @param client - API client instance
|
|
207
|
+
* @returns Current auth status
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* const status = await getAuthStatus(client);
|
|
212
|
+
* if (status.authenticated) {
|
|
213
|
+
* console.log(`Logged in as ${status.user.username}`);
|
|
214
|
+
* }
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
export async function getAuthStatus(client: EnactApiClient): Promise<AuthStatus> {
|
|
218
|
+
if (!client.isAuthenticated()) {
|
|
219
|
+
return { authenticated: false };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const user = await getCurrentUser(client);
|
|
224
|
+
return {
|
|
225
|
+
authenticated: true,
|
|
226
|
+
user: {
|
|
227
|
+
username: user.username,
|
|
228
|
+
email: user.email,
|
|
229
|
+
namespaces: user.namespaces,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
} catch {
|
|
233
|
+
// Token might be invalid/expired
|
|
234
|
+
return { authenticated: false };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get user profile by username (v2)
|
|
240
|
+
*
|
|
241
|
+
* @param client - API client instance
|
|
242
|
+
* @param username - Username to look up
|
|
243
|
+
* @returns User profile info
|
|
244
|
+
*/
|
|
245
|
+
export async function getUserProfile(
|
|
246
|
+
client: EnactApiClient,
|
|
247
|
+
username: string
|
|
248
|
+
): Promise<{
|
|
249
|
+
username: string;
|
|
250
|
+
displayName?: string | undefined;
|
|
251
|
+
avatarUrl?: string | undefined;
|
|
252
|
+
createdAt: Date;
|
|
253
|
+
toolsCount?: number | undefined;
|
|
254
|
+
}> {
|
|
255
|
+
const response = await client.get<{
|
|
256
|
+
username: string;
|
|
257
|
+
display_name?: string | undefined;
|
|
258
|
+
avatar_url?: string | undefined;
|
|
259
|
+
created_at: string;
|
|
260
|
+
tools_count?: number | undefined;
|
|
261
|
+
}>(`/users/${username}`);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
username: response.data.username,
|
|
265
|
+
displayName: response.data.display_name,
|
|
266
|
+
avatarUrl: response.data.avatar_url,
|
|
267
|
+
createdAt: new Date(response.data.created_at),
|
|
268
|
+
toolsCount: response.data.tools_count,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Submit feedback for a tool
|
|
274
|
+
*
|
|
275
|
+
* @param client - API client instance (must be authenticated)
|
|
276
|
+
* @param name - Tool name
|
|
277
|
+
* @param rating - Rating (1-5)
|
|
278
|
+
* @param version - Version being rated
|
|
279
|
+
* @param comment - Optional comment
|
|
280
|
+
*/
|
|
281
|
+
export async function submitFeedback(
|
|
282
|
+
client: EnactApiClient,
|
|
283
|
+
name: string,
|
|
284
|
+
rating: number,
|
|
285
|
+
version: string,
|
|
286
|
+
comment?: string
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
await client.post(`/tools/${name}/feedback`, {
|
|
289
|
+
rating,
|
|
290
|
+
version,
|
|
291
|
+
comment,
|
|
292
|
+
});
|
|
293
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enact Registry API Client
|
|
3
|
+
* Core HTTP client for interacting with the Enact registry
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ApiError, RateLimitInfo } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default registry URL
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_REGISTRY_URL = "https://siikwkfgsmouioodghho.supabase.co/functions/v1";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* API client configuration options
|
|
15
|
+
*/
|
|
16
|
+
export interface ApiClientOptions {
|
|
17
|
+
/** Registry base URL (default: https://siikwkfgsmouioodghho.supabase.co/functions/v1) */
|
|
18
|
+
baseUrl?: string | undefined;
|
|
19
|
+
/** Authentication token */
|
|
20
|
+
authToken?: string | undefined;
|
|
21
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
22
|
+
timeout?: number | undefined;
|
|
23
|
+
/** Number of retry attempts for failed requests (default: 3) */
|
|
24
|
+
retries?: number | undefined;
|
|
25
|
+
/** User agent string */
|
|
26
|
+
userAgent?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* API response wrapper
|
|
31
|
+
*/
|
|
32
|
+
export interface ApiResponse<T> {
|
|
33
|
+
/** Response data */
|
|
34
|
+
data: T;
|
|
35
|
+
/** HTTP status code */
|
|
36
|
+
status: number;
|
|
37
|
+
/** Rate limit information */
|
|
38
|
+
rateLimit?: RateLimitInfo | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* API request error
|
|
43
|
+
*/
|
|
44
|
+
export class ApiRequestError extends Error {
|
|
45
|
+
/** HTTP status code */
|
|
46
|
+
readonly status: number;
|
|
47
|
+
/** API error code */
|
|
48
|
+
readonly code: string;
|
|
49
|
+
/** Original error response */
|
|
50
|
+
readonly response?: ApiError | undefined;
|
|
51
|
+
|
|
52
|
+
constructor(message: string, status: number, code: string, response?: ApiError) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "ApiRequestError";
|
|
55
|
+
this.status = status;
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.response = response;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse rate limit headers from response
|
|
63
|
+
*/
|
|
64
|
+
function parseRateLimitHeaders(headers: Headers): RateLimitInfo | undefined {
|
|
65
|
+
const limit = headers.get("X-RateLimit-Limit");
|
|
66
|
+
const remaining = headers.get("X-RateLimit-Remaining");
|
|
67
|
+
const reset = headers.get("X-RateLimit-Reset");
|
|
68
|
+
|
|
69
|
+
if (limit && remaining && reset) {
|
|
70
|
+
return {
|
|
71
|
+
limit: Number.parseInt(limit, 10),
|
|
72
|
+
remaining: Number.parseInt(remaining, 10),
|
|
73
|
+
reset: Number.parseInt(reset, 10),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Enact Registry API Client
|
|
82
|
+
*/
|
|
83
|
+
export class EnactApiClient {
|
|
84
|
+
private readonly baseUrl: string;
|
|
85
|
+
private readonly timeout: number;
|
|
86
|
+
private readonly maxRetries: number;
|
|
87
|
+
private readonly userAgent: string;
|
|
88
|
+
private authToken: string | undefined;
|
|
89
|
+
|
|
90
|
+
constructor(options: ApiClientOptions = {}) {
|
|
91
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_REGISTRY_URL;
|
|
92
|
+
this.timeout = options.timeout ?? 30000;
|
|
93
|
+
this.maxRetries = options.retries ?? 3;
|
|
94
|
+
this.userAgent = options.userAgent ?? "enact-cli/0.1.0";
|
|
95
|
+
this.authToken = options.authToken;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set authentication token
|
|
100
|
+
*/
|
|
101
|
+
setAuthToken(token: string | undefined): void {
|
|
102
|
+
this.authToken = token;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get current authentication token
|
|
107
|
+
*/
|
|
108
|
+
getAuthToken(): string | undefined {
|
|
109
|
+
return this.authToken;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the base URL for the registry
|
|
114
|
+
*/
|
|
115
|
+
getBaseUrl(): string {
|
|
116
|
+
return this.baseUrl;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the user agent string
|
|
121
|
+
*/
|
|
122
|
+
getUserAgent(): string {
|
|
123
|
+
return this.userAgent;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if client is authenticated
|
|
128
|
+
*/
|
|
129
|
+
isAuthenticated(): boolean {
|
|
130
|
+
return this.authToken !== undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build headers for a request
|
|
135
|
+
*/
|
|
136
|
+
private buildHeaders(contentType?: string): Headers {
|
|
137
|
+
const headers = new Headers();
|
|
138
|
+
headers.set("User-Agent", this.userAgent);
|
|
139
|
+
headers.set("Accept", "application/json");
|
|
140
|
+
|
|
141
|
+
if (contentType) {
|
|
142
|
+
headers.set("Content-Type", contentType);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.authToken) {
|
|
146
|
+
headers.set("Authorization", `Bearer ${this.authToken}`);
|
|
147
|
+
// Supabase Edge Functions also need the apikey header
|
|
148
|
+
headers.set("apikey", this.authToken);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return headers;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Make an HTTP request with retry logic
|
|
156
|
+
*/
|
|
157
|
+
private async request<T>(
|
|
158
|
+
method: string,
|
|
159
|
+
path: string,
|
|
160
|
+
options: {
|
|
161
|
+
body?: unknown;
|
|
162
|
+
contentType?: string;
|
|
163
|
+
retryCount?: number;
|
|
164
|
+
} = {}
|
|
165
|
+
): Promise<ApiResponse<T>> {
|
|
166
|
+
const url = `${this.baseUrl}${path}`;
|
|
167
|
+
const retryCount = options.retryCount ?? 0;
|
|
168
|
+
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(url, {
|
|
174
|
+
method,
|
|
175
|
+
headers: this.buildHeaders(options.contentType),
|
|
176
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
177
|
+
signal: controller.signal,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
|
|
182
|
+
const rateLimit = parseRateLimitHeaders(response.headers);
|
|
183
|
+
|
|
184
|
+
// Handle rate limiting with retry
|
|
185
|
+
if (response.status === 429 && retryCount < this.maxRetries) {
|
|
186
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
187
|
+
const delay = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : 1000 * (retryCount + 1);
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
189
|
+
return this.request<T>(method, path, { ...options, retryCount: retryCount + 1 });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle error responses
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
let errorData: ApiError | undefined;
|
|
195
|
+
try {
|
|
196
|
+
errorData = (await response.json()) as ApiError;
|
|
197
|
+
} catch {
|
|
198
|
+
// Response body might not be JSON
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const code = errorData?.error?.code ?? "unknown";
|
|
202
|
+
const message = errorData?.error?.message ?? `HTTP ${response.status}`;
|
|
203
|
+
|
|
204
|
+
throw new ApiRequestError(message, response.status, code, errorData);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Handle 204 No Content
|
|
208
|
+
if (response.status === 204) {
|
|
209
|
+
return {
|
|
210
|
+
data: undefined as T,
|
|
211
|
+
status: response.status,
|
|
212
|
+
rateLimit,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Parse JSON response
|
|
217
|
+
let data: T;
|
|
218
|
+
try {
|
|
219
|
+
const text = await response.text();
|
|
220
|
+
if (!text || text.trim() === "") {
|
|
221
|
+
throw new ApiRequestError(
|
|
222
|
+
"Server returned empty response",
|
|
223
|
+
response.status,
|
|
224
|
+
"empty_response"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
data = JSON.parse(text) as T;
|
|
228
|
+
} catch (parseError) {
|
|
229
|
+
if (parseError instanceof ApiRequestError) {
|
|
230
|
+
throw parseError;
|
|
231
|
+
}
|
|
232
|
+
throw new ApiRequestError(
|
|
233
|
+
"Server returned invalid JSON response",
|
|
234
|
+
response.status,
|
|
235
|
+
"invalid_json"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { data, status: response.status, rateLimit };
|
|
240
|
+
} catch (error) {
|
|
241
|
+
clearTimeout(timeoutId);
|
|
242
|
+
|
|
243
|
+
// Handle network errors with retry
|
|
244
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
245
|
+
if (retryCount < this.maxRetries) {
|
|
246
|
+
const delay = 1000 * (retryCount + 1);
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
248
|
+
return this.request<T>(method, path, { ...options, retryCount: retryCount + 1 });
|
|
249
|
+
}
|
|
250
|
+
throw new ApiRequestError("Request timeout", 0, "timeout");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Re-throw ApiRequestErrors
|
|
254
|
+
if (error instanceof ApiRequestError) {
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Wrap other errors
|
|
259
|
+
throw new ApiRequestError(
|
|
260
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
261
|
+
0,
|
|
262
|
+
"network_error"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* GET request
|
|
269
|
+
*/
|
|
270
|
+
async get<T>(path: string): Promise<ApiResponse<T>> {
|
|
271
|
+
return this.request<T>("GET", path);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* POST request
|
|
276
|
+
*/
|
|
277
|
+
async post<T>(path: string, body?: unknown): Promise<ApiResponse<T>> {
|
|
278
|
+
return this.request<T>("POST", path, {
|
|
279
|
+
body,
|
|
280
|
+
contentType: "application/json",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* PUT request
|
|
286
|
+
*/
|
|
287
|
+
async put<T>(path: string, body?: unknown): Promise<ApiResponse<T>> {
|
|
288
|
+
return this.request<T>("PUT", path, {
|
|
289
|
+
body,
|
|
290
|
+
contentType: "application/json",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* DELETE request
|
|
296
|
+
*/
|
|
297
|
+
async delete<T>(path: string): Promise<ApiResponse<T>> {
|
|
298
|
+
return this.request<T>("DELETE", path);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Download a file (returns raw response for streaming)
|
|
303
|
+
*/
|
|
304
|
+
async download(path: string): Promise<Response> {
|
|
305
|
+
const url = `${this.baseUrl}${path}`;
|
|
306
|
+
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout * 10); // Longer timeout for downloads
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetch(url, {
|
|
312
|
+
method: "GET",
|
|
313
|
+
headers: this.buildHeaders(),
|
|
314
|
+
signal: controller.signal,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
clearTimeout(timeoutId);
|
|
318
|
+
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
throw new ApiRequestError(
|
|
321
|
+
`Download failed: HTTP ${response.status}`,
|
|
322
|
+
response.status,
|
|
323
|
+
"download_error"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return response;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
clearTimeout(timeoutId);
|
|
330
|
+
|
|
331
|
+
if (error instanceof ApiRequestError) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
throw new ApiRequestError(
|
|
336
|
+
error instanceof Error ? error.message : "Download failed",
|
|
337
|
+
0,
|
|
338
|
+
"download_error"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Create a new API client instance
|
|
346
|
+
*/
|
|
347
|
+
export function createApiClient(options?: ApiClientOptions): EnactApiClient {
|
|
348
|
+
return new EnactApiClient(options);
|
|
349
|
+
}
|