@crosspost/sdk 0.1.5 → 0.1.7

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,166 +1,133 @@
1
- import { ApiError, ApiErrorCode, Platform, PlatformError } from '@crosspost/types';
1
+ import { ApiErrorCode, type ErrorDetails, type StatusCode } from '@crosspost/types';
2
2
 
3
3
  /**
4
- * Error categories grouped by type
4
+ * CrosspostError class for SDK error handling
5
5
  */
6
- export const ERROR_CATEGORIES = {
7
- AUTH: [
8
- ApiErrorCode.UNAUTHORIZED,
9
- ApiErrorCode.FORBIDDEN,
10
- ],
11
- VALIDATION: [
12
- ApiErrorCode.VALIDATION_ERROR,
13
- ApiErrorCode.INVALID_REQUEST,
14
- ],
15
- NETWORK: [
16
- ApiErrorCode.NETWORK_ERROR,
17
- ],
18
- PLATFORM: [
19
- ApiErrorCode.PLATFORM_ERROR,
20
- ApiErrorCode.PLATFORM_UNAVAILABLE,
21
- ],
22
- CONTENT: [
23
- ApiErrorCode.CONTENT_POLICY_VIOLATION,
24
- ApiErrorCode.DUPLICATE_CONTENT,
25
- ],
26
- RATE_LIMIT: [
27
- ApiErrorCode.RATE_LIMITED,
28
- ],
29
- POST: [
30
- ApiErrorCode.POST_CREATION_FAILED,
31
- ApiErrorCode.THREAD_CREATION_FAILED,
32
- ApiErrorCode.POST_DELETION_FAILED,
33
- ApiErrorCode.POST_INTERACTION_FAILED,
34
- ],
35
- MEDIA: [
36
- ApiErrorCode.MEDIA_UPLOAD_FAILED,
37
- ],
38
- };
39
-
40
- /**
41
- * Check if an error belongs to a specific category
42
- *
43
- * @param error The error to check
44
- * @param category The category to check against
45
- * @returns True if the error belongs to the category, false otherwise
46
- */
47
- export function isErrorOfCategory(error: unknown, category: ApiErrorCode[]): boolean {
48
- if (error instanceof ApiError) {
49
- return category.includes(error.code);
6
+ export class CrosspostError extends Error {
7
+ readonly code: ApiErrorCode;
8
+ readonly status: StatusCode;
9
+ readonly details?: ErrorDetails;
10
+ readonly recoverable: boolean;
11
+
12
+ constructor(
13
+ message: string,
14
+ code: ApiErrorCode,
15
+ status: StatusCode,
16
+ details?: ErrorDetails,
17
+ recoverable: boolean = false,
18
+ ) {
19
+ super(message);
20
+ this.name = 'CrosspostError';
21
+ this.code = code;
22
+ this.status = status;
23
+ this.details = details;
24
+ this.recoverable = recoverable;
50
25
  }
51
26
 
52
- if (error instanceof PlatformError) {
53
- return category.includes(error.code);
27
+ /**
28
+ * Get platform from details if available
29
+ */
30
+ get platform(): string | undefined {
31
+ return this.details?.platform;
54
32
  }
55
33
 
56
- // Fallback for error-like objects with code property
57
- if (error && typeof error === 'object' && 'code' in error) {
58
- return category.includes((error as any).code);
34
+ /**
35
+ * Get userId from details if available
36
+ */
37
+ get userId(): string | undefined {
38
+ return this.details?.userId;
59
39
  }
40
+ }
60
41
 
61
- return false;
42
+ /**
43
+ * Check if an error is a specific type based on its code
44
+ */
45
+ function isErrorCode(error: unknown, codes: ApiErrorCode[]): boolean {
46
+ return error instanceof CrosspostError && codes.includes(error.code);
62
47
  }
63
48
 
64
49
  /**
65
50
  * Check if an error is an authentication error
66
- *
67
- * @param error The error to check
68
- * @returns True if the error is an authentication error, false otherwise
69
51
  */
70
52
  export function isAuthError(error: unknown): boolean {
71
- return isErrorOfCategory(error, ERROR_CATEGORIES.AUTH);
53
+ return isErrorCode(error, [
54
+ ApiErrorCode.UNAUTHORIZED,
55
+ ApiErrorCode.FORBIDDEN,
56
+ ]);
72
57
  }
73
58
 
74
59
  /**
75
60
  * Check if an error is a validation error
76
- *
77
- * @param error The error to check
78
- * @returns True if the error is a validation error, false otherwise
79
61
  */
80
62
  export function isValidationError(error: unknown): boolean {
81
- return isErrorOfCategory(error, ERROR_CATEGORIES.VALIDATION);
63
+ return isErrorCode(error, [
64
+ ApiErrorCode.VALIDATION_ERROR,
65
+ ApiErrorCode.INVALID_REQUEST,
66
+ ]);
82
67
  }
83
68
 
84
69
  /**
85
70
  * Check if an error is a network error
86
- *
87
- * @param error The error to check
88
- * @returns True if the error is a network error, false otherwise
89
71
  */
90
72
  export function isNetworkError(error: unknown): boolean {
91
- return isErrorOfCategory(error, ERROR_CATEGORIES.NETWORK);
73
+ return isErrorCode(error, [
74
+ ApiErrorCode.NETWORK_ERROR,
75
+ ApiErrorCode.PLATFORM_UNAVAILABLE,
76
+ ]);
92
77
  }
93
78
 
94
79
  /**
95
80
  * Check if an error is a platform error
96
- *
97
- * @param error The error to check
98
- * @returns True if the error is a platform error, false otherwise
99
81
  */
100
82
  export function isPlatformError(error: unknown): boolean {
101
- return isErrorOfCategory(error, ERROR_CATEGORIES.PLATFORM) || error instanceof PlatformError;
83
+ return error instanceof CrosspostError && !!error.details?.platform;
102
84
  }
103
85
 
104
86
  /**
105
87
  * Check if an error is a content policy error
106
- *
107
- * @param error The error to check
108
- * @returns True if the error is a content policy error, false otherwise
109
88
  */
110
89
  export function isContentError(error: unknown): boolean {
111
- return isErrorOfCategory(error, ERROR_CATEGORIES.CONTENT);
90
+ return isErrorCode(error, [
91
+ ApiErrorCode.CONTENT_POLICY_VIOLATION,
92
+ ApiErrorCode.DUPLICATE_CONTENT,
93
+ ]);
112
94
  }
113
95
 
114
96
  /**
115
97
  * Check if an error is a rate limit error
116
- *
117
- * @param error The error to check
118
- * @returns True if the error is a rate limit error, false otherwise
119
98
  */
120
99
  export function isRateLimitError(error: unknown): boolean {
121
- return isErrorOfCategory(error, ERROR_CATEGORIES.RATE_LIMIT);
100
+ return isErrorCode(error, [ApiErrorCode.RATE_LIMITED]);
122
101
  }
123
102
 
124
103
  /**
125
104
  * Check if an error is a post-related error
126
- *
127
- * @param error The error to check
128
- * @returns True if the error is a post-related error, false otherwise
129
105
  */
130
106
  export function isPostError(error: unknown): boolean {
131
- return isErrorOfCategory(error, ERROR_CATEGORIES.POST);
107
+ return isErrorCode(error, [
108
+ ApiErrorCode.POST_CREATION_FAILED,
109
+ ApiErrorCode.THREAD_CREATION_FAILED,
110
+ ApiErrorCode.POST_DELETION_FAILED,
111
+ ApiErrorCode.POST_INTERACTION_FAILED,
112
+ ]);
132
113
  }
133
114
 
134
115
  /**
135
116
  * Check if an error is a media-related error
136
- *
137
- * @param error The error to check
138
- * @returns True if the error is a media-related error, false otherwise
139
117
  */
140
118
  export function isMediaError(error: unknown): boolean {
141
- return isErrorOfCategory(error, ERROR_CATEGORIES.MEDIA);
119
+ return isErrorCode(error, [ApiErrorCode.MEDIA_UPLOAD_FAILED]);
142
120
  }
143
121
 
144
122
  /**
145
123
  * Check if an error is recoverable
146
- *
147
- * @param error The error to check
148
- * @returns True if the error is recoverable, false otherwise
149
124
  */
150
125
  export function isRecoverableError(error: unknown): boolean {
151
- if (error instanceof ApiError || error instanceof PlatformError) {
152
- return error.recoverable;
153
- }
154
-
155
- return false;
126
+ return error instanceof CrosspostError && error.recoverable;
156
127
  }
157
128
 
158
129
  /**
159
130
  * Get a user-friendly error message
160
- *
161
- * @param error The error to get the message from
162
- * @param defaultMessage The default message to return if no message is found
163
- * @returns The error message
164
131
  */
165
132
  export function getErrorMessage(
166
133
  error: unknown,
@@ -169,50 +136,47 @@ export function getErrorMessage(
169
136
  if (error instanceof Error) {
170
137
  return error.message || defaultMessage;
171
138
  }
172
-
173
- if (typeof error === 'string') {
174
- return error;
175
- }
176
-
177
- if (error && typeof error === 'object' && 'message' in error) {
178
- return (error as any).message || defaultMessage;
179
- }
180
-
181
139
  return defaultMessage;
182
140
  }
183
141
 
184
142
  /**
185
- * Extract error details if available
186
- *
187
- * @param error The error to extract details from
188
- * @returns The error details or undefined if none are found
143
+ * Get error details if available
189
144
  */
190
- export function getErrorDetails(error: unknown): Record<string, any> | undefined {
191
- if (error instanceof ApiError || error instanceof PlatformError) {
145
+ export function getErrorDetails(error: unknown): ErrorDetails | undefined {
146
+ if (error instanceof CrosspostError) {
192
147
  return error.details;
193
148
  }
194
-
195
- if (error && typeof error === 'object' && 'details' in error) {
196
- return (error as any).details;
197
- }
198
-
199
149
  return undefined;
200
150
  }
201
151
 
152
+ /**
153
+ * Create a new error
154
+ */
155
+ function createError(
156
+ message: string,
157
+ code: ApiErrorCode,
158
+ status: StatusCode,
159
+ details?: ErrorDetails,
160
+ recoverable: boolean = false,
161
+ ): CrosspostError {
162
+ return new CrosspostError(
163
+ message,
164
+ code,
165
+ status,
166
+ details,
167
+ recoverable,
168
+ );
169
+ }
170
+
202
171
  /**
203
172
  * Enrich an error with additional context
204
- *
205
- * @param error The error to enrich
206
- * @param context The context to add to the error
207
- * @returns The enriched error
208
173
  */
209
174
  export function enrichErrorWithContext(
210
175
  error: unknown,
211
- context: Record<string, any>,
176
+ context: Record<string, unknown>,
212
177
  ): Error {
213
- if (error instanceof ApiError) {
214
- // Create a new ApiError with the merged details since details is read-only
215
- return new ApiError(
178
+ if (error instanceof CrosspostError) {
179
+ return createError(
216
180
  error.message,
217
181
  error.code,
218
182
  error.status,
@@ -221,42 +185,22 @@ export function enrichErrorWithContext(
221
185
  );
222
186
  }
223
187
 
224
- if (error instanceof PlatformError) {
225
- // Create a new PlatformError with the merged details since details is read-only
226
- return new PlatformError(
227
- error.message,
228
- error.platform,
229
- error.code,
230
- error.recoverable,
231
- error.originalError,
232
- error.status,
233
- error.userId,
234
- { ...(error.details || {}), ...context },
235
- );
236
- }
237
-
238
- // For regular errors or non-Error objects, create a new ApiError with the context
188
+ // For regular errors or non-Error objects, create a new CrosspostError
239
189
  const errorMessage = error instanceof Error ? error.message : String(error);
240
- return new ApiError(
190
+ return createError(
241
191
  errorMessage || 'An error occurred',
242
192
  ApiErrorCode.INTERNAL_ERROR,
243
193
  500,
244
194
  { originalError: error, ...context },
245
- false,
246
195
  );
247
196
  }
248
197
 
249
198
  /**
250
199
  * Wrapper for API calls with consistent error handling
251
- *
252
- * @param apiCall The API call to wrap
253
- * @param context Optional context to add to any errors
254
- * @returns The result of the API call
255
- * @throws An enriched error if the API call fails
256
200
  */
257
201
  export async function apiWrapper<T>(
258
202
  apiCall: () => Promise<T>,
259
- context?: Record<string, any>,
203
+ context?: Record<string, unknown>,
260
204
  ): Promise<T> {
261
205
  try {
262
206
  return await apiCall();
@@ -273,26 +217,25 @@ export async function apiWrapper<T>(
273
217
  // If JSON parsing fails, create a generic error
274
218
  if (jsonError instanceof Error && jsonError.name === 'SyntaxError') {
275
219
  throw enrichErrorWithContext(
276
- new ApiError(
220
+ createError(
277
221
  `API request failed with status ${error.status} and non-JSON response`,
278
222
  ApiErrorCode.NETWORK_ERROR,
279
- error.status as any,
223
+ error.status as StatusCode,
280
224
  { originalResponse: error.statusText },
281
225
  ),
282
226
  context || {},
283
227
  );
284
228
  }
285
- // If it's already an enriched error from handleErrorResponse, just throw it
286
229
  throw jsonError;
287
230
  }
288
231
  }
289
232
 
290
- // If it's already an ApiError or PlatformError, just add context
291
- if (error instanceof ApiError || error instanceof PlatformError) {
233
+ // If it's already a CrosspostError, just add context
234
+ if (error instanceof CrosspostError) {
292
235
  throw enrichErrorWithContext(error, context || {});
293
236
  }
294
237
 
295
- // Otherwise wrap it in an ApiError
238
+ // Otherwise wrap it in a CrosspostError
296
239
  throw enrichErrorWithContext(
297
240
  error instanceof Error ? error : new Error(String(error)),
298
241
  context || {},
@@ -302,65 +245,81 @@ export async function apiWrapper<T>(
302
245
 
303
246
  /**
304
247
  * Handles error responses from the API and converts them to appropriate error objects.
305
- *
306
- * @param data The error response data
307
- * @param status The HTTP status code
308
- * @returns An ApiError or PlatformError instance
309
248
  */
310
- export function handleErrorResponse(data: any, status: number): ApiError | PlatformError {
311
- // Safely access nested error properties
312
- const errorData = data?.error || {};
313
- const message = errorData?.message || data?.message || 'An API error occurred';
314
-
315
- // Ensure code is a valid ApiErrorCode or default
316
- const codeString = errorData?.code || data?.code || ApiErrorCode.UNKNOWN_ERROR;
317
- const code = Object.values(ApiErrorCode).includes(codeString as ApiErrorCode)
318
- ? codeString as ApiErrorCode
319
- : ApiErrorCode.UNKNOWN_ERROR;
320
-
321
- const details = errorData?.details || data?.details || {};
322
- const recoverable = errorData?.recoverable ?? data?.recoverable ?? false;
323
- const platform = errorData?.platform || data?.platform;
249
+ export function handleErrorResponse(
250
+ data: any,
251
+ status: number,
252
+ ): CrosspostError {
253
+ // Validate response format
254
+ if (!data || typeof data !== 'object' || !('success' in data)) {
255
+ return createError(
256
+ 'Invalid API response format',
257
+ ApiErrorCode.INTERNAL_ERROR,
258
+ status as StatusCode,
259
+ { originalResponse: data },
260
+ );
261
+ }
324
262
 
325
- // Add original response data to details if not already present
326
- const enhancedDetails = { ...details };
327
- if (typeof enhancedDetails === 'object' && !enhancedDetails.originalResponse) {
328
- enhancedDetails.originalResponse = data; // Include the raw error payload for debugging
263
+ // Check for errors array
264
+ if (!data.errors || !Array.isArray(data.errors) || data.errors.length === 0) {
265
+ return createError(
266
+ 'Invalid error response format',
267
+ ApiErrorCode.INTERNAL_ERROR,
268
+ status as StatusCode,
269
+ { originalResponse: data },
270
+ );
329
271
  }
330
272
 
331
- if (platform && Object.values(Platform).includes(platform as Platform)) {
332
- // If platform is specified and valid, it's a PlatformError
333
- return new PlatformError(
334
- message,
335
- platform as Platform,
336
- code, // Use the parsed code
337
- status as any, // Cast status
338
- enhancedDetails,
339
- recoverable,
273
+ // Handle single vs multiple errors
274
+ if (data.errors.length === 1) {
275
+ const errorDetail = data.errors[0];
276
+
277
+ // Validate error detail structure
278
+ if (!errorDetail.message || !errorDetail.code) {
279
+ return createError(
280
+ 'Invalid error detail format',
281
+ ApiErrorCode.INTERNAL_ERROR,
282
+ status as StatusCode,
283
+ { originalResponse: data },
284
+ );
285
+ }
286
+
287
+ // Merge meta data from the API response with error details
288
+ const finalDetails = {
289
+ ...(errorDetail.details || {}),
290
+ ...(data.meta || {}),
291
+ };
292
+
293
+ return createError(
294
+ errorDetail.message,
295
+ errorDetail.code as ApiErrorCode,
296
+ status as StatusCode,
297
+ finalDetails,
298
+ errorDetail.recoverable ?? false,
340
299
  );
341
300
  } else {
342
- // Otherwise, it's a general ApiError
343
- return new ApiError(
344
- message,
345
- code, // Use the parsed code
346
- status as any, // Cast status
347
- enhancedDetails,
348
- recoverable,
301
+ // Multiple errors - return first error with details about others
302
+ const firstError = data.errors[0];
303
+ return createError(
304
+ 'Multiple errors occurred',
305
+ firstError.code as ApiErrorCode,
306
+ status as StatusCode,
307
+ {
308
+ errors: data.errors,
309
+ ...(data.meta || {}),
310
+ originalResponse: data,
311
+ },
312
+ false,
349
313
  );
350
314
  }
351
315
  }
352
316
 
353
317
  /**
354
318
  * Creates a network error with appropriate details
355
- *
356
- * @param error The original error
357
- * @param url The request URL
358
- * @param timeout The request timeout
359
- * @returns An ApiError instance
360
319
  */
361
- export function createNetworkError(error: unknown, url: string, timeout: number): ApiError {
320
+ export function createNetworkError(error: unknown, url: string, timeout: number): CrosspostError {
362
321
  if (error instanceof DOMException && error.name === 'AbortError') {
363
- return new ApiError(
322
+ return createError(
364
323
  `Request timed out after ${timeout}ms`,
365
324
  ApiErrorCode.NETWORK_ERROR,
366
325
  408,
@@ -368,7 +327,7 @@ export function createNetworkError(error: unknown, url: string, timeout: number)
368
327
  );
369
328
  }
370
329
 
371
- return new ApiError(
330
+ return createError(
372
331
  error instanceof Error ? error.message : 'An unexpected error occurred during the request',
373
332
  ApiErrorCode.INTERNAL_ERROR,
374
333
  500,