@crosspost/sdk 0.1.6 → 0.1.8

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