@crosspost/sdk 0.1.6 → 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.
- package/dist/index.cjs +376 -665
- package/dist/index.d.cts +32 -79
- package/dist/index.d.ts +32 -79
- package/dist/index.js +371 -639
- package/package.json +1 -1
- package/src/api/auth.ts +7 -7
- package/src/api/post.ts +3 -15
- package/src/api/system.ts +3 -3
- package/src/core/request.ts +33 -32
- package/src/index.ts +0 -10
- package/src/utils/error.ts +150 -247
package/src/utils/error.ts
CHANGED
@@ -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
|
-
*
|
4
|
+
* CrosspostError class for SDK error handling
|
13
5
|
*/
|
14
|
-
export
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
61
|
-
|
27
|
+
/**
|
28
|
+
* Get platform from details if available
|
29
|
+
*/
|
30
|
+
get platform(): string | undefined {
|
31
|
+
return this.details?.platform;
|
62
32
|
}
|
63
33
|
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
*
|
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):
|
199
|
-
if (error instanceof
|
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,
|
176
|
+
context: Record<string, unknown>,
|
220
177
|
): Error {
|
221
|
-
if (error instanceof
|
222
|
-
|
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
|
-
|
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
|
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,
|
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
|
-
|
220
|
+
createError(
|
285
221
|
`API request failed with status ${error.status} and non-JSON response`,
|
286
222
|
ApiErrorCode.NETWORK_ERROR,
|
287
|
-
error.status as
|
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
|
299
|
-
if (error instanceof
|
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
|
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
|
-
):
|
322
|
-
//
|
323
|
-
if (data
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
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
|
-
|
386
|
-
|
297
|
+
finalDetails,
|
298
|
+
errorDetail.recoverable ?? false,
|
387
299
|
);
|
388
300
|
} else {
|
389
|
-
return
|
390
|
-
|
391
|
-
|
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
|
-
|
394
|
-
|
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
|
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
|
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,
|