@crosspost/sdk 0.2.12 → 0.3.1
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/README.md +149 -222
- package/dist/index.cjs +130 -155
- package/dist/index.d.cts +13 -31
- package/dist/index.d.ts +13 -31
- package/dist/index.js +120 -154
- package/package.json +3 -2
- package/src/api/auth.ts +1 -1
- package/src/core/client.ts +12 -35
- package/src/core/config.ts +3 -5
- package/src/core/request.ts +151 -122
- package/src/index.ts +0 -1
- package/src/utils/error.ts +0 -48
package/src/core/request.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import retry from 'async-retry';
|
|
1
2
|
import { ApiErrorCode, type ApiResponse, type StatusCode } from '@crosspost/types';
|
|
2
|
-
import { createAuthToken, type NearAuthData } from 'near-sign-verify';
|
|
3
3
|
import {
|
|
4
4
|
createNetworkError,
|
|
5
5
|
CrosspostError,
|
|
@@ -16,15 +16,13 @@ export interface RequestOptions {
|
|
|
16
16
|
*/
|
|
17
17
|
baseUrl: URL;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* Required for non-GET requests, optional for GET requests
|
|
19
|
+
* Auth token from near-sign-verify
|
|
21
20
|
*/
|
|
22
|
-
|
|
21
|
+
authToken?: string;
|
|
23
22
|
/**
|
|
24
|
-
* NEAR account ID for
|
|
25
|
-
* If not provided, will use account_id from nearAuthData
|
|
23
|
+
* NEAR account ID for simple GET request authentication
|
|
26
24
|
*/
|
|
27
|
-
|
|
25
|
+
accountId?: string;
|
|
28
26
|
/**
|
|
29
27
|
* Request timeout in milliseconds
|
|
30
28
|
*/
|
|
@@ -76,122 +74,153 @@ export async function makeRequest<
|
|
|
76
74
|
url,
|
|
77
75
|
};
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
77
|
+
return retry(
|
|
78
|
+
async (bail, _attemptNumber) => {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const headers: Record<string, string> = {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
'Accept': 'application/json',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// For GET requests, use X-Near-Account header if available
|
|
89
|
+
if (method === 'GET') {
|
|
90
|
+
const accountId = options.accountId;
|
|
91
|
+
if (!accountId) {
|
|
92
|
+
throw new CrosspostError(
|
|
93
|
+
'No NEAR account provided for GET request',
|
|
94
|
+
ApiErrorCode.UNAUTHORIZED,
|
|
95
|
+
401,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
headers['X-Near-Account'] = accountId;
|
|
99
|
+
} else {
|
|
100
|
+
// For non-GET requests, require authToken
|
|
101
|
+
if (!options.authToken) {
|
|
102
|
+
throw new CrosspostError(
|
|
103
|
+
'Auth token required for non-GET request',
|
|
104
|
+
ApiErrorCode.UNAUTHORIZED,
|
|
105
|
+
401,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
headers['Authorization'] = `Bearer ${options.authToken}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const requestOptions: RequestInit = {
|
|
112
|
+
method,
|
|
113
|
+
headers,
|
|
114
|
+
body: method !== 'GET' && data ? JSON.stringify(data) : undefined,
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const response = await fetch(url, requestOptions);
|
|
119
|
+
clearTimeout(timeoutId);
|
|
120
|
+
|
|
121
|
+
let responseData: ApiResponse<TResponse>;
|
|
122
|
+
try {
|
|
123
|
+
responseData = await response.json();
|
|
124
|
+
} catch (jsonError) {
|
|
125
|
+
// JSON parsing failed - try to get response text for context
|
|
126
|
+
let responseText: string | undefined;
|
|
127
|
+
try {
|
|
128
|
+
responseText = await response.text();
|
|
129
|
+
} catch (_) { /* ignore */ }
|
|
130
|
+
|
|
131
|
+
throw new CrosspostError(
|
|
132
|
+
`API request failed with status ${response.status} and non-JSON response`,
|
|
133
|
+
ApiErrorCode.INVALID_RESPONSE,
|
|
134
|
+
response.status as StatusCode,
|
|
135
|
+
{
|
|
136
|
+
originalStatusText: response.statusText,
|
|
137
|
+
originalError: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
|
138
|
+
responseText,
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle non-ok responses (4xx/5xx)
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw handleErrorResponse(responseData, response.status);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate success response structure
|
|
149
|
+
if (
|
|
150
|
+
!responseData || typeof responseData !== 'object' || !('success' in responseData) ||
|
|
151
|
+
!('meta' in responseData)
|
|
152
|
+
) {
|
|
153
|
+
throw new CrosspostError(
|
|
154
|
+
'Invalid response format from API',
|
|
155
|
+
ApiErrorCode.INVALID_RESPONSE,
|
|
156
|
+
response.status as StatusCode,
|
|
157
|
+
{ responseData },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (responseData.success) {
|
|
162
|
+
return responseData as ApiResponse<TResponse>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// If we get here, we have response.ok but success: false
|
|
166
|
+
// This is unexpected - treat as an error
|
|
167
|
+
throw handleErrorResponse(responseData, response.status);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
clearTimeout(timeoutId);
|
|
170
|
+
|
|
171
|
+
// Check if the error is one we want to retry on
|
|
172
|
+
if (
|
|
173
|
+
error instanceof TypeError ||
|
|
174
|
+
(error instanceof DOMException && error.name === 'AbortError')
|
|
175
|
+
) {
|
|
176
|
+
// If it's a retryable error, async-retry will handle the retry.
|
|
177
|
+
// Re-throw it here so async-retry knows the attempt failed.
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// If it's not a retryable error, or if retries are exhausted,
|
|
182
|
+
// enrich and bail out of retries.
|
|
183
|
+
if (error instanceof CrosspostError) {
|
|
184
|
+
// Enrich CrosspostError with request context and bail
|
|
185
|
+
const enrichedError = enrichErrorWithContext(error, context);
|
|
186
|
+
bail(enrichedError); // bail will throw this error
|
|
187
|
+
throw enrichedError; // Redundant but satisfies some linters/type checkers
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
error instanceof TypeError ||
|
|
192
|
+
(error instanceof DOMException && error.name === 'AbortError')
|
|
193
|
+
) {
|
|
194
|
+
const networkError = createNetworkError(error, url.toString(), options.timeout);
|
|
195
|
+
const enrichedNetworkError = enrichErrorWithContext(networkError, context);
|
|
196
|
+
bail(enrichedNetworkError);
|
|
197
|
+
throw enrichedNetworkError;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// For any other non-retryable errors, wrap them, enrich, and bail
|
|
201
|
+
const wrappedError = new CrosspostError(
|
|
202
|
+
error instanceof Error ? error.message : String(error),
|
|
203
|
+
ApiErrorCode.INTERNAL_ERROR,
|
|
204
|
+
500,
|
|
205
|
+
{ originalError: String(error) },
|
|
96
206
|
);
|
|
207
|
+
const enrichedWrappedError = enrichErrorWithContext(wrappedError, context);
|
|
208
|
+
bail(enrichedWrappedError); // bail will throw this error
|
|
209
|
+
throw enrichedWrappedError; // Redundant
|
|
97
210
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
retries: 3,
|
|
214
|
+
factor: 1,
|
|
215
|
+
minTimeout: 1000,
|
|
216
|
+
maxTimeout: 1000,
|
|
217
|
+
onRetry: (error, attempt) => {
|
|
218
|
+
console.warn(
|
|
219
|
+
`Attempt ${attempt} failed for ${context.method} ${context.path}: ${
|
|
220
|
+
(error instanceof Error) ? error.message : 'Unknown error'
|
|
221
|
+
}. Retrying...`,
|
|
106
222
|
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const requestOptions: RequestInit = {
|
|
112
|
-
method,
|
|
113
|
-
headers,
|
|
114
|
-
body: method !== 'GET' && data ? JSON.stringify(data) : undefined,
|
|
115
|
-
signal: controller.signal,
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const response = await fetch(url, requestOptions);
|
|
119
|
-
clearTimeout(timeoutId);
|
|
120
|
-
|
|
121
|
-
let responseData: ApiResponse<TResponse>;
|
|
122
|
-
try {
|
|
123
|
-
responseData = await response.json();
|
|
124
|
-
} catch (jsonError) {
|
|
125
|
-
// JSON parsing failed - try to get response text for context
|
|
126
|
-
let responseText: string | undefined;
|
|
127
|
-
try {
|
|
128
|
-
responseText = await response.text();
|
|
129
|
-
} catch (_) { /* ignore */ }
|
|
130
|
-
|
|
131
|
-
throw new CrosspostError(
|
|
132
|
-
`API request failed with status ${response.status} and non-JSON response`,
|
|
133
|
-
ApiErrorCode.INVALID_RESPONSE,
|
|
134
|
-
response.status as StatusCode,
|
|
135
|
-
{
|
|
136
|
-
originalStatusText: response.statusText,
|
|
137
|
-
originalError: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
|
138
|
-
responseText,
|
|
139
|
-
},
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Handle non-ok responses (4xx/5xx)
|
|
144
|
-
if (!response.ok) {
|
|
145
|
-
throw handleErrorResponse(responseData, response.status);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Validate success response structure
|
|
149
|
-
if (
|
|
150
|
-
!responseData || typeof responseData !== 'object' || !('success' in responseData) ||
|
|
151
|
-
!('meta' in responseData)
|
|
152
|
-
) {
|
|
153
|
-
throw new CrosspostError(
|
|
154
|
-
'Invalid response format from API',
|
|
155
|
-
ApiErrorCode.INVALID_RESPONSE,
|
|
156
|
-
response.status as StatusCode,
|
|
157
|
-
{ responseData },
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (responseData.success) {
|
|
162
|
-
return responseData as ApiResponse<TResponse>;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// If we get here, we have response.ok but success: false
|
|
166
|
-
// This is unexpected - treat as an error
|
|
167
|
-
throw handleErrorResponse(responseData, response.status);
|
|
168
|
-
} catch (error) {
|
|
169
|
-
clearTimeout(timeoutId);
|
|
170
|
-
|
|
171
|
-
if (error instanceof CrosspostError) {
|
|
172
|
-
// Enrich CrosspostError with request context
|
|
173
|
-
throw enrichErrorWithContext(error, context);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Handle network errors (including timeouts)
|
|
177
|
-
if (
|
|
178
|
-
error instanceof TypeError || (error instanceof DOMException && error.name === 'AbortError')
|
|
179
|
-
) {
|
|
180
|
-
throw enrichErrorWithContext(
|
|
181
|
-
createNetworkError(error, url.toString(), options.timeout),
|
|
182
|
-
context,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// For any other errors, wrap them with context
|
|
187
|
-
throw enrichErrorWithContext(
|
|
188
|
-
new CrosspostError(
|
|
189
|
-
error instanceof Error ? error.message : String(error),
|
|
190
|
-
ApiErrorCode.INTERNAL_ERROR,
|
|
191
|
-
500,
|
|
192
|
-
{ originalError: String(error) },
|
|
193
|
-
),
|
|
194
|
-
context,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
);
|
|
197
226
|
}
|
package/src/index.ts
CHANGED
package/src/utils/error.ts
CHANGED
|
@@ -195,54 +195,6 @@ export function enrichErrorWithContext(
|
|
|
195
195
|
);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
/**
|
|
199
|
-
* Wrapper for API calls with consistent error handling
|
|
200
|
-
*/
|
|
201
|
-
export async function apiWrapper<T>(
|
|
202
|
-
apiCall: () => Promise<T>,
|
|
203
|
-
context?: Record<string, unknown>,
|
|
204
|
-
): Promise<T> {
|
|
205
|
-
try {
|
|
206
|
-
return await apiCall();
|
|
207
|
-
} catch (error) {
|
|
208
|
-
// If it's a Response object, use handleErrorResponse
|
|
209
|
-
if (error instanceof Response) {
|
|
210
|
-
try {
|
|
211
|
-
const errorData = await error.json();
|
|
212
|
-
throw enrichErrorWithContext(
|
|
213
|
-
handleErrorResponse(errorData, error.status),
|
|
214
|
-
context || {},
|
|
215
|
-
);
|
|
216
|
-
} catch (jsonError) {
|
|
217
|
-
// If JSON parsing fails, create a generic error
|
|
218
|
-
if (jsonError instanceof Error && jsonError.name === 'SyntaxError') {
|
|
219
|
-
throw enrichErrorWithContext(
|
|
220
|
-
createError(
|
|
221
|
-
`API request failed with status ${error.status} and non-JSON response`,
|
|
222
|
-
ApiErrorCode.NETWORK_ERROR,
|
|
223
|
-
error.status as StatusCode,
|
|
224
|
-
{ originalResponse: error.statusText },
|
|
225
|
-
),
|
|
226
|
-
context || {},
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
throw jsonError;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// If it's already a CrosspostError, just add context
|
|
234
|
-
if (error instanceof CrosspostError) {
|
|
235
|
-
throw enrichErrorWithContext(error, context || {});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Otherwise wrap it in a CrosspostError
|
|
239
|
-
throw enrichErrorWithContext(
|
|
240
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
241
|
-
context || {},
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
198
|
/**
|
|
247
199
|
* Handles error responses from the API and converts them to appropriate error objects.
|
|
248
200
|
*/
|