@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.
@@ -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
- * NEAR authentication data for generating auth tokens
20
- * Required for non-GET requests, optional for GET requests
19
+ * Auth token from near-sign-verify
21
20
  */
22
- nearAuthData?: NearAuthData;
21
+ authToken?: string;
23
22
  /**
24
- * NEAR account ID for simplified GET request authentication
25
- * If not provided, will use account_id from nearAuthData
23
+ * NEAR account ID for simple GET request authentication
26
24
  */
27
- nearAccount?: string;
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
- 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 nearAccount = options.nearAccount || options.nearAuthData?.account_id;
91
- if (!nearAccount) {
92
- throw new CrosspostError(
93
- 'No NEAR account provided for GET request',
94
- ApiErrorCode.UNAUTHORIZED,
95
- 401,
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
- headers['X-Near-Account'] = nearAccount;
99
- } else {
100
- // For non-GET requests, require nearAuthData
101
- if (!options.nearAuthData) {
102
- throw new CrosspostError(
103
- 'NEAR authentication data required for non-GET request',
104
- ApiErrorCode.UNAUTHORIZED,
105
- 401,
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
- headers['Authorization'] = `Bearer ${createAuthToken(options.nearAuthData)}`;
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
@@ -7,7 +7,6 @@ export { PostApi } from './api/post.ts';
7
7
  export { SystemApi } from './api/system.ts';
8
8
 
9
9
  export {
10
- apiWrapper,
11
10
  CrosspostError,
12
11
  getErrorDetails,
13
12
  getErrorMessage,
@@ -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
  */