@crosspost/sdk 0.1.7 → 0.1.9
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/dist/index.cjs +180 -93
- package/dist/index.d.cts +44 -25
- package/dist/index.d.ts +44 -25
- package/dist/index.js +177 -93
- package/package.json +1 -1
- package/src/api/activity.ts +3 -3
- package/src/api/auth.ts +44 -18
- package/src/api/post.ts +7 -7
- package/src/api/system.ts +12 -5
- package/src/core/client.ts +40 -3
- package/src/core/config.ts +0 -6
- package/src/core/request.ts +125 -111
package/src/api/auth.ts
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
import type {
|
2
|
-
|
2
|
+
AuthCallbackResponse,
|
3
|
+
AuthInitRequest,
|
3
4
|
AuthRevokeResponse,
|
5
|
+
AuthStatusParams,
|
4
6
|
AuthStatusResponse,
|
7
|
+
AuthTokenRequest,
|
8
|
+
AuthUrlResponse,
|
9
|
+
ConnectedAccount,
|
5
10
|
ConnectedAccountsResponse,
|
11
|
+
NearAuthorizationRequest,
|
6
12
|
NearAuthorizationResponse,
|
13
|
+
NearUnauthorizationResponse,
|
7
14
|
Platform,
|
8
15
|
} from '@crosspost/types';
|
9
16
|
import { makeRequest, type RequestOptions } from '../core/request.ts';
|
@@ -27,7 +34,7 @@ export class AuthApi {
|
|
27
34
|
* @returns A promise resolving with the authorization response.
|
28
35
|
*/
|
29
36
|
async authorizeNearAccount(): Promise<NearAuthorizationResponse> {
|
30
|
-
return makeRequest<NearAuthorizationResponse>(
|
37
|
+
return makeRequest<NearAuthorizationResponse, NearAuthorizationRequest>(
|
31
38
|
'POST',
|
32
39
|
'/auth/authorize/near',
|
33
40
|
this.options,
|
@@ -40,7 +47,7 @@ export class AuthApi {
|
|
40
47
|
* @returns A promise resolving with the authorization status response.
|
41
48
|
*/
|
42
49
|
async getNearAuthorizationStatus(): Promise<NearAuthorizationResponse> {
|
43
|
-
return makeRequest<NearAuthorizationResponse>(
|
50
|
+
return makeRequest<NearAuthorizationResponse, never>(
|
44
51
|
'GET',
|
45
52
|
'/auth/authorize/near/status',
|
46
53
|
this.options,
|
@@ -56,9 +63,9 @@ export class AuthApi {
|
|
56
63
|
*/
|
57
64
|
async loginToPlatform(
|
58
65
|
platform: Platform,
|
59
|
-
options?:
|
60
|
-
): Promise<
|
61
|
-
return makeRequest<
|
66
|
+
options?: AuthInitRequest,
|
67
|
+
): Promise<AuthUrlResponse> {
|
68
|
+
return makeRequest<AuthUrlResponse, AuthInitRequest>(
|
62
69
|
'POST',
|
63
70
|
`/auth/${platform}/login`,
|
64
71
|
this.options,
|
@@ -69,26 +76,29 @@ export class AuthApi {
|
|
69
76
|
/**
|
70
77
|
* Refreshes the authentication token for the specified platform.
|
71
78
|
* @param platform The target platform.
|
72
|
-
* @returns A promise resolving with the refresh response.
|
79
|
+
* @returns A promise resolving with the refresh response containing updated auth details.
|
73
80
|
*/
|
74
|
-
async refreshToken(platform: Platform): Promise<
|
75
|
-
return makeRequest<
|
81
|
+
async refreshToken(platform: Platform, userId: string): Promise<AuthCallbackResponse> {
|
82
|
+
return makeRequest<AuthCallbackResponse, AuthTokenRequest>(
|
76
83
|
'POST',
|
77
84
|
`/auth/${platform}/refresh`,
|
78
85
|
this.options,
|
86
|
+
{ userId },
|
79
87
|
);
|
80
88
|
}
|
81
89
|
|
82
90
|
/**
|
83
91
|
* Refreshes the user's profile information from the specified platform.
|
84
92
|
* @param platform The target platform.
|
85
|
-
* @
|
93
|
+
* @param userId The user ID on the platform
|
94
|
+
* @returns A promise resolving with the updated account profile information.
|
86
95
|
*/
|
87
|
-
async refreshProfile(platform: Platform): Promise<
|
88
|
-
return makeRequest<
|
96
|
+
async refreshProfile(platform: Platform, userId: string): Promise<ConnectedAccount> {
|
97
|
+
return makeRequest<ConnectedAccount, AuthTokenRequest>(
|
89
98
|
'POST',
|
90
99
|
`/auth/${platform}/refresh-profile`,
|
91
100
|
this.options,
|
101
|
+
{ userId },
|
92
102
|
);
|
93
103
|
}
|
94
104
|
|
@@ -97,11 +107,26 @@ export class AuthApi {
|
|
97
107
|
* @param platform The target platform.
|
98
108
|
* @returns A promise resolving with the authentication status response.
|
99
109
|
*/
|
100
|
-
async getAuthStatus(platform: Platform): Promise<AuthStatusResponse> {
|
101
|
-
return makeRequest<AuthStatusResponse>(
|
110
|
+
async getAuthStatus(platform: Platform, userId: string): Promise<AuthStatusResponse> {
|
111
|
+
return makeRequest<AuthStatusResponse, never, AuthStatusParams>(
|
102
112
|
'GET',
|
103
|
-
`/auth/${platform}/status`,
|
113
|
+
`/auth/${platform}/status/${userId}`,
|
104
114
|
this.options,
|
115
|
+
undefined,
|
116
|
+
{ platform, userId },
|
117
|
+
);
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
* Unauthorizes a NEAR account from using the service
|
122
|
+
* @returns A promise resolving with the unauthorized response
|
123
|
+
*/
|
124
|
+
async unauthorizeNear(): Promise<NearUnauthorizationResponse> {
|
125
|
+
return makeRequest<NearUnauthorizationResponse, NearAuthorizationRequest>(
|
126
|
+
'DELETE',
|
127
|
+
'/auth/unauthorize/near',
|
128
|
+
this.options,
|
129
|
+
{},
|
105
130
|
);
|
106
131
|
}
|
107
132
|
|
@@ -110,11 +135,12 @@ export class AuthApi {
|
|
110
135
|
* @param platform The target platform.
|
111
136
|
* @returns A promise resolving with the revocation response.
|
112
137
|
*/
|
113
|
-
async revokeAuth(platform: Platform): Promise<AuthRevokeResponse> {
|
114
|
-
return makeRequest<AuthRevokeResponse>(
|
138
|
+
async revokeAuth(platform: Platform, userId: string): Promise<AuthRevokeResponse> {
|
139
|
+
return makeRequest<AuthRevokeResponse, AuthTokenRequest>(
|
115
140
|
'DELETE',
|
116
141
|
`/auth/${platform}/revoke`,
|
117
142
|
this.options,
|
143
|
+
{ userId },
|
118
144
|
);
|
119
145
|
}
|
120
146
|
|
@@ -123,7 +149,7 @@ export class AuthApi {
|
|
123
149
|
* @returns A promise resolving with the list of connected accounts.
|
124
150
|
*/
|
125
151
|
async getConnectedAccounts(): Promise<ConnectedAccountsResponse> {
|
126
|
-
return makeRequest<ConnectedAccountsResponse>(
|
152
|
+
return makeRequest<ConnectedAccountsResponse, never>(
|
127
153
|
'GET',
|
128
154
|
'/auth/accounts',
|
129
155
|
this.options,
|
package/src/api/post.ts
CHANGED
@@ -36,7 +36,7 @@ export class PostApi {
|
|
36
36
|
* @returns A promise resolving with the post creation response.
|
37
37
|
*/
|
38
38
|
async createPost(request: CreatePostRequest): Promise<CreatePostResponse> {
|
39
|
-
return makeRequest<CreatePostResponse>(
|
39
|
+
return makeRequest<CreatePostResponse, CreatePostRequest>(
|
40
40
|
'POST',
|
41
41
|
'/api/post',
|
42
42
|
this.options,
|
@@ -50,7 +50,7 @@ export class PostApi {
|
|
50
50
|
* @returns A promise resolving with the repost response.
|
51
51
|
*/
|
52
52
|
async repost(request: RepostRequest): Promise<RepostResponse> {
|
53
|
-
return makeRequest<RepostResponse>(
|
53
|
+
return makeRequest<RepostResponse, RepostRequest>(
|
54
54
|
'POST',
|
55
55
|
'/api/post/repost',
|
56
56
|
this.options,
|
@@ -64,7 +64,7 @@ export class PostApi {
|
|
64
64
|
* @returns A promise resolving with the quote post response.
|
65
65
|
*/
|
66
66
|
async quotePost(request: QuotePostRequest): Promise<QuotePostResponse> {
|
67
|
-
return makeRequest<QuotePostResponse>(
|
67
|
+
return makeRequest<QuotePostResponse, QuotePostRequest>(
|
68
68
|
'POST',
|
69
69
|
'/api/post/quote',
|
70
70
|
this.options,
|
@@ -78,7 +78,7 @@ export class PostApi {
|
|
78
78
|
* @returns A promise resolving with the reply response.
|
79
79
|
*/
|
80
80
|
async replyToPost(request: ReplyToPostRequest): Promise<ReplyToPostResponse> {
|
81
|
-
return makeRequest<ReplyToPostResponse>(
|
81
|
+
return makeRequest<ReplyToPostResponse, ReplyToPostRequest>(
|
82
82
|
'POST',
|
83
83
|
'/api/post/reply',
|
84
84
|
this.options,
|
@@ -92,7 +92,7 @@ export class PostApi {
|
|
92
92
|
* @returns A promise resolving with the like response.
|
93
93
|
*/
|
94
94
|
async likePost(request: LikePostRequest): Promise<LikePostResponse> {
|
95
|
-
return makeRequest<LikePostResponse>(
|
95
|
+
return makeRequest<LikePostResponse, LikePostRequest>(
|
96
96
|
'POST',
|
97
97
|
`/api/post/like`,
|
98
98
|
this.options,
|
@@ -106,7 +106,7 @@ export class PostApi {
|
|
106
106
|
* @returns A promise resolving with the unlike response.
|
107
107
|
*/
|
108
108
|
async unlikePost(request: UnlikePostRequest): Promise<UnlikePostResponse> {
|
109
|
-
return makeRequest<UnlikePostResponse>(
|
109
|
+
return makeRequest<UnlikePostResponse, UnlikePostRequest>(
|
110
110
|
'DELETE',
|
111
111
|
`/api/post/like`,
|
112
112
|
this.options,
|
@@ -120,7 +120,7 @@ export class PostApi {
|
|
120
120
|
* @returns A promise resolving with the delete response.
|
121
121
|
*/
|
122
122
|
async deletePost(request: DeletePostRequest): Promise<DeletePostResponse> {
|
123
|
-
return makeRequest<DeletePostResponse>(
|
123
|
+
return makeRequest<DeletePostResponse, DeletePostRequest>(
|
124
124
|
'DELETE',
|
125
125
|
`/api/post`,
|
126
126
|
this.options,
|
package/src/api/system.ts
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
-
import type {
|
1
|
+
import type {
|
2
|
+
EndpointRateLimitResponse,
|
3
|
+
HealthStatus,
|
4
|
+
RateLimitEndpointParam,
|
5
|
+
RateLimitResponse,
|
6
|
+
} from '@crosspost/types';
|
2
7
|
import { makeRequest, type RequestOptions } from '../core/request.ts';
|
3
8
|
|
4
9
|
/**
|
@@ -21,7 +26,7 @@ export class SystemApi {
|
|
21
26
|
* @returns A promise resolving with the rate limit response
|
22
27
|
*/
|
23
28
|
async getRateLimits(): Promise<RateLimitResponse> {
|
24
|
-
return makeRequest<RateLimitResponse>(
|
29
|
+
return makeRequest<RateLimitResponse, never>(
|
25
30
|
'GET',
|
26
31
|
'/api/rate-limit',
|
27
32
|
this.options,
|
@@ -34,10 +39,12 @@ export class SystemApi {
|
|
34
39
|
* @returns A promise resolving with the endpoint rate limit response
|
35
40
|
*/
|
36
41
|
async getEndpointRateLimit(endpoint: string): Promise<EndpointRateLimitResponse> {
|
37
|
-
return makeRequest<EndpointRateLimitResponse>(
|
42
|
+
return makeRequest<EndpointRateLimitResponse, never, RateLimitEndpointParam>(
|
38
43
|
'GET',
|
39
44
|
`/api/rate-limit/${endpoint}`,
|
40
45
|
this.options,
|
46
|
+
undefined,
|
47
|
+
{ endpoint },
|
41
48
|
);
|
42
49
|
}
|
43
50
|
|
@@ -45,8 +52,8 @@ export class SystemApi {
|
|
45
52
|
* Gets the health status of the API
|
46
53
|
* @returns A promise resolving with the health status
|
47
54
|
*/
|
48
|
-
async getHealthStatus(): Promise<
|
49
|
-
return makeRequest<
|
55
|
+
async getHealthStatus(): Promise<HealthStatus> {
|
56
|
+
return makeRequest<HealthStatus, never>(
|
50
57
|
'GET',
|
51
58
|
'/health',
|
52
59
|
this.options,
|
package/src/core/client.ts
CHANGED
@@ -24,14 +24,12 @@ export class CrosspostClient {
|
|
24
24
|
constructor(config: CrosspostClientConfig = {}) {
|
25
25
|
const baseUrl = config.baseUrl || DEFAULT_CONFIG.baseUrl; // you can deploy your own
|
26
26
|
const timeout = config.timeout || DEFAULT_CONFIG.timeout;
|
27
|
-
const retries = config.retries ?? DEFAULT_CONFIG.retries;
|
28
27
|
|
29
28
|
const nearAuthData = config.nearAuthData;
|
30
29
|
|
31
30
|
this.options = {
|
32
31
|
baseUrl,
|
33
32
|
timeout,
|
34
|
-
retries,
|
35
33
|
nearAuthData,
|
36
34
|
};
|
37
35
|
|
@@ -43,17 +41,56 @@ export class CrosspostClient {
|
|
43
41
|
|
44
42
|
/**
|
45
43
|
* Sets the authentication data (signature) for the client
|
44
|
+
* Required for non-GET requests
|
46
45
|
* @param nearAuthData The NEAR authentication data
|
47
46
|
*/
|
48
47
|
public setAuthentication(nearAuthData: NearAuthData): void {
|
49
48
|
this.options.nearAuthData = nearAuthData;
|
49
|
+
// Also set nearAccount from nearAuthData if not already set
|
50
|
+
if (!this.options.nearAccount) {
|
51
|
+
this.options.nearAccount = nearAuthData.account_id;
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Sets the NEAR account ID for simplified GET request authentication
|
57
|
+
* If not set, will use account_id from nearAuthData
|
58
|
+
* @param nearAccount The NEAR account ID
|
59
|
+
*/
|
60
|
+
public setNearAccount(nearAccount: string): void {
|
61
|
+
this.options.nearAccount = nearAccount;
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Gets the current NEAR account ID being used for authentication
|
66
|
+
* @returns The NEAR account ID from nearAccount or nearAuthData
|
67
|
+
*/
|
68
|
+
public getNearAccount(): string | undefined {
|
69
|
+
return this.options.nearAccount || this.options.nearAuthData?.account_id;
|
50
70
|
}
|
51
71
|
|
52
72
|
/**
|
53
73
|
* Checks if authentication data (signature) exists on client
|
54
|
-
* @
|
74
|
+
* @returns true if nearAuthData is set (required for non-GET requests)
|
55
75
|
*/
|
56
76
|
public isAuthenticated(): boolean {
|
57
77
|
return !!this.options.nearAuthData;
|
58
78
|
}
|
79
|
+
|
80
|
+
/**
|
81
|
+
* Checks if a NEAR account is set for GET request authentication
|
82
|
+
* @returns true if either nearAccount or nearAuthData.account_id is set
|
83
|
+
*/
|
84
|
+
public hasNearAccount(): boolean {
|
85
|
+
return !!(this.options.nearAccount || this.options.nearAuthData?.account_id);
|
86
|
+
}
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Clears all authentication data from the client
|
90
|
+
* This will prevent all requests from working until new authentication is set
|
91
|
+
*/
|
92
|
+
public clear(): void {
|
93
|
+
this.options.nearAuthData = undefined;
|
94
|
+
this.options.nearAccount = undefined;
|
95
|
+
}
|
59
96
|
}
|
package/src/core/config.ts
CHANGED
@@ -18,11 +18,6 @@ export interface CrosspostClientConfig {
|
|
18
18
|
* @default 30000
|
19
19
|
*/
|
20
20
|
timeout?: number;
|
21
|
-
/**
|
22
|
-
* Number of retries for failed requests (specifically for network errors or 5xx status codes)
|
23
|
-
* @default 2
|
24
|
-
*/
|
25
|
-
retries?: number;
|
26
21
|
}
|
27
22
|
|
28
23
|
/**
|
@@ -31,5 +26,4 @@ export interface CrosspostClientConfig {
|
|
31
26
|
export const DEFAULT_CONFIG: Required<Omit<CrosspostClientConfig, 'nearAuthData'>> = {
|
32
27
|
baseUrl: 'https://open-crosspost-proxy.deno.dev/',
|
33
28
|
timeout: 30000,
|
34
|
-
retries: 2,
|
35
29
|
};
|
package/src/core/request.ts
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
import { ApiErrorCode, type StatusCode } from '@crosspost/types';
|
2
2
|
import { createAuthToken, type NearAuthData } from 'near-sign-verify';
|
3
3
|
import {
|
4
|
-
apiWrapper,
|
5
4
|
createNetworkError,
|
6
5
|
CrosspostError,
|
6
|
+
enrichErrorWithContext,
|
7
7
|
handleErrorResponse,
|
8
8
|
} from '../utils/error.ts';
|
9
9
|
|
@@ -17,21 +17,22 @@ export interface RequestOptions {
|
|
17
17
|
baseUrl: string;
|
18
18
|
/**
|
19
19
|
* NEAR authentication data for generating auth tokens
|
20
|
-
*
|
20
|
+
* Required for non-GET requests, optional for GET requests
|
21
21
|
*/
|
22
22
|
nearAuthData?: NearAuthData;
|
23
23
|
/**
|
24
|
-
*
|
24
|
+
* NEAR account ID for simplified GET request authentication
|
25
|
+
* If not provided, will use account_id from nearAuthData
|
25
26
|
*/
|
26
|
-
|
27
|
+
nearAccount?: string;
|
27
28
|
/**
|
28
|
-
*
|
29
|
+
* Request timeout in milliseconds
|
29
30
|
*/
|
30
|
-
|
31
|
+
timeout: number;
|
31
32
|
}
|
32
33
|
|
33
34
|
/**
|
34
|
-
* Makes a request to the API with
|
35
|
+
* Makes a request to the API with error handling
|
35
36
|
*
|
36
37
|
* @param method The HTTP method
|
37
38
|
* @param path The API path
|
@@ -39,17 +40,21 @@ export interface RequestOptions {
|
|
39
40
|
* @param data Optional request data
|
40
41
|
* @returns A promise resolving with the response data
|
41
42
|
*/
|
42
|
-
export async function makeRequest<
|
43
|
+
export async function makeRequest<
|
44
|
+
TResponse,
|
45
|
+
TRequest = unknown,
|
46
|
+
TQuery = unknown,
|
47
|
+
>(
|
43
48
|
method: string,
|
44
49
|
path: string,
|
45
50
|
options: RequestOptions,
|
46
|
-
data?:
|
47
|
-
query?:
|
48
|
-
): Promise<
|
51
|
+
data?: TRequest,
|
52
|
+
query?: TQuery,
|
53
|
+
): Promise<TResponse> {
|
49
54
|
let url = `${options.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
50
55
|
|
51
56
|
// Add query parameters if provided
|
52
|
-
if (query && Object.keys(query).length > 0) {
|
57
|
+
if (query && typeof query === 'object' && Object.keys(query).length > 0) {
|
53
58
|
const queryParams = new URLSearchParams();
|
54
59
|
for (const [key, value] of Object.entries(query)) {
|
55
60
|
if (value !== undefined && value !== null) {
|
@@ -67,109 +72,118 @@ export async function makeRequest<T>(
|
|
67
72
|
method,
|
68
73
|
path,
|
69
74
|
url,
|
70
|
-
retries: options.retries,
|
71
75
|
};
|
72
76
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
77
|
+
const controller = new AbortController();
|
78
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
79
|
+
|
80
|
+
try {
|
81
|
+
const headers: Record<string, string> = {
|
82
|
+
'Content-Type': 'application/json',
|
83
|
+
'Accept': 'application/json',
|
84
|
+
};
|
85
|
+
|
86
|
+
// For GET requests, use X-Near-Account header if available
|
87
|
+
if (method === 'GET') {
|
88
|
+
const nearAccount = options.nearAccount || options.nearAuthData?.account_id;
|
89
|
+
if (!nearAccount) {
|
90
|
+
throw new CrosspostError(
|
91
|
+
'No NEAR account provided for GET request',
|
92
|
+
ApiErrorCode.UNAUTHORIZED,
|
93
|
+
401,
|
94
|
+
);
|
95
|
+
}
|
96
|
+
headers['X-Near-Account'] = nearAccount;
|
97
|
+
} else {
|
98
|
+
// For non-GET requests, require nearAuthData
|
99
|
+
if (!options.nearAuthData) {
|
100
|
+
throw new CrosspostError(
|
101
|
+
'NEAR authentication data required for non-GET request',
|
102
|
+
ApiErrorCode.UNAUTHORIZED,
|
103
|
+
401,
|
104
|
+
);
|
105
|
+
}
|
106
|
+
headers['Authorization'] = `Bearer ${createAuthToken(options.nearAuthData)}`;
|
107
|
+
}
|
79
108
|
|
109
|
+
const requestOptions: RequestInit = {
|
110
|
+
method,
|
111
|
+
headers,
|
112
|
+
body: method !== 'GET' && data ? JSON.stringify(data) : undefined,
|
113
|
+
signal: controller.signal,
|
114
|
+
};
|
115
|
+
|
116
|
+
const response = await fetch(url, requestOptions);
|
117
|
+
clearTimeout(timeoutId);
|
118
|
+
|
119
|
+
let responseData: any;
|
120
|
+
try {
|
121
|
+
responseData = await response.json();
|
122
|
+
} catch (jsonError) {
|
123
|
+
// JSON parsing failed - try to get response text for context
|
124
|
+
let responseText: string | undefined;
|
80
125
|
try {
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
}
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
}
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
responseData = await response.json();
|
100
|
-
} catch (jsonError) {
|
101
|
-
// If JSON parsing fails, did API throw an error?
|
102
|
-
if (!response.ok) {
|
103
|
-
throw new CrosspostError(
|
104
|
-
`API request failed with status ${response.status} and non-JSON response`,
|
105
|
-
ApiErrorCode.NETWORK_ERROR,
|
106
|
-
response.status as StatusCode,
|
107
|
-
{ originalStatusText: response.statusText },
|
108
|
-
);
|
109
|
-
}
|
110
|
-
// Otherwise, throw a custom error
|
111
|
-
throw new CrosspostError(
|
112
|
-
`Failed to parse JSON response: ${
|
113
|
-
jsonError instanceof Error ? jsonError.message : String(jsonError)
|
114
|
-
}`,
|
115
|
-
ApiErrorCode.INTERNAL_ERROR,
|
116
|
-
response.status as StatusCode,
|
117
|
-
);
|
118
|
-
}
|
119
|
-
|
120
|
-
if (!response.ok) {
|
121
|
-
lastError = handleErrorResponse(responseData, response.status);
|
122
|
-
// Only retry rate limit errors
|
123
|
-
const shouldRetry = lastError instanceof CrosspostError &&
|
124
|
-
lastError.code === ApiErrorCode.RATE_LIMITED;
|
125
|
-
if (shouldRetry && attempt < options.retries) {
|
126
|
-
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
|
127
|
-
continue; // Retry
|
128
|
-
}
|
129
|
-
throw lastError; // Throw error if not retrying or retries exhausted
|
130
|
-
}
|
131
|
-
|
132
|
-
// Handle response based on success flag
|
133
|
-
if (responseData && typeof responseData === 'object' && 'success' in responseData) {
|
134
|
-
if (responseData.success) {
|
135
|
-
// Success response - return the data
|
136
|
-
return responseData.data as T;
|
137
|
-
} else {
|
138
|
-
// Error response - handle with our error utilities
|
139
|
-
lastError = handleErrorResponse(responseData, response.status);
|
140
|
-
// Only retry rate limit errors
|
141
|
-
const shouldRetry = lastError instanceof CrosspostError &&
|
142
|
-
lastError.code === ApiErrorCode.RATE_LIMITED;
|
143
|
-
if (shouldRetry && attempt < options.retries) {
|
144
|
-
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
|
145
|
-
continue; // Retry
|
146
|
-
}
|
147
|
-
throw lastError;
|
148
|
-
}
|
149
|
-
}
|
150
|
-
} catch (error) {
|
151
|
-
clearTimeout(timeoutId); // Clear timeout on error
|
152
|
-
lastError = error instanceof Error ? error : new Error(String(error)); // Store the error
|
153
|
-
|
154
|
-
// Handle fetch/network errors specifically for retries
|
155
|
-
const isNetworkError = error instanceof TypeError ||
|
156
|
-
(error instanceof DOMException && error.name === 'AbortError');
|
157
|
-
if (isNetworkError && attempt < options.retries) {
|
158
|
-
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
|
159
|
-
continue; // Retry network error
|
160
|
-
}
|
161
|
-
|
162
|
-
// If it's not a known ApiError/PlatformError, wrap it
|
163
|
-
if (!(error instanceof CrosspostError)) {
|
164
|
-
throw createNetworkError(error, url, options.timeout);
|
165
|
-
}
|
166
|
-
|
167
|
-
throw error; // Re-throw known ApiError or final network error
|
168
|
-
}
|
126
|
+
responseText = await response.text();
|
127
|
+
} catch (_) { /* ignore */ }
|
128
|
+
|
129
|
+
throw new CrosspostError(
|
130
|
+
`API request failed with status ${response.status} and non-JSON response`,
|
131
|
+
ApiErrorCode.INVALID_RESPONSE,
|
132
|
+
response.status as StatusCode,
|
133
|
+
{
|
134
|
+
originalStatusText: response.statusText,
|
135
|
+
originalError: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
136
|
+
responseText,
|
137
|
+
},
|
138
|
+
);
|
139
|
+
}
|
140
|
+
|
141
|
+
// Handle non-ok responses (4xx/5xx)
|
142
|
+
if (!response.ok) {
|
143
|
+
throw handleErrorResponse(responseData, response.status);
|
169
144
|
}
|
170
145
|
|
171
|
-
//
|
172
|
-
|
173
|
-
new CrosspostError(
|
174
|
-
|
146
|
+
// Validate success response structure
|
147
|
+
if (!responseData || typeof responseData !== 'object' || !('success' in responseData)) {
|
148
|
+
throw new CrosspostError(
|
149
|
+
'Invalid success response format from API',
|
150
|
+
ApiErrorCode.INVALID_RESPONSE,
|
151
|
+
response.status as StatusCode,
|
152
|
+
{ responseData },
|
153
|
+
);
|
154
|
+
}
|
155
|
+
|
156
|
+
if (responseData.success) {
|
157
|
+
return responseData.data as TResponse;
|
158
|
+
}
|
159
|
+
|
160
|
+
// If we get here, we have response.ok but success: false
|
161
|
+
// This is unexpected - treat as an error
|
162
|
+
throw handleErrorResponse(responseData, response.status);
|
163
|
+
} catch (error) {
|
164
|
+
clearTimeout(timeoutId);
|
165
|
+
|
166
|
+
if (error instanceof CrosspostError) {
|
167
|
+
// Enrich CrosspostError with request context
|
168
|
+
throw enrichErrorWithContext(error, context);
|
169
|
+
}
|
170
|
+
|
171
|
+
// Handle network errors (including timeouts)
|
172
|
+
if (
|
173
|
+
error instanceof TypeError || (error instanceof DOMException && error.name === 'AbortError')
|
174
|
+
) {
|
175
|
+
throw enrichErrorWithContext(createNetworkError(error, url, options.timeout), context);
|
176
|
+
}
|
177
|
+
|
178
|
+
// For any other errors, wrap them with context
|
179
|
+
throw enrichErrorWithContext(
|
180
|
+
new CrosspostError(
|
181
|
+
error instanceof Error ? error.message : String(error),
|
182
|
+
ApiErrorCode.INTERNAL_ERROR,
|
183
|
+
500,
|
184
|
+
{ originalError: String(error) },
|
185
|
+
),
|
186
|
+
context,
|
187
|
+
);
|
188
|
+
}
|
175
189
|
}
|