@crosspost/sdk 0.1.2
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 +274 -0
- package/dist/index.cjs +5485 -0
- package/dist/index.d.cts +236 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.js +5350 -0
- package/mod.ts +9 -0
- package/package.json +53 -0
- package/src/api/auth.ts +132 -0
- package/src/api/post.ts +142 -0
- package/src/core/client.ts +58 -0
- package/src/core/config.ts +35 -0
- package/src/core/request.ts +158 -0
- package/src/index.ts +28 -0
- package/src/utils/cookie.ts +75 -0
- package/src/utils/error.ts +77 -0
package/mod.ts
ADDED
package/package.json
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
{
|
2
|
+
"name": "@crosspost/sdk",
|
3
|
+
"version": "0.1.2",
|
4
|
+
"description": "SDK for interacting with the Crosspost API",
|
5
|
+
"type": "module",
|
6
|
+
"main": "dist/index.cjs",
|
7
|
+
"module": "dist/index.js",
|
8
|
+
"types": "dist/index.d.ts",
|
9
|
+
"exports": {
|
10
|
+
".": {
|
11
|
+
"types": "./dist/index.d.ts",
|
12
|
+
"import": "./dist/index.js",
|
13
|
+
"require": "./dist/index.cjs"
|
14
|
+
}
|
15
|
+
},
|
16
|
+
"files": [
|
17
|
+
"dist",
|
18
|
+
"src",
|
19
|
+
"mod.ts"
|
20
|
+
],
|
21
|
+
"scripts": {
|
22
|
+
"build": "npm run build:node",
|
23
|
+
"build:node": "tsup src/index.ts --format cjs,esm --dts",
|
24
|
+
"clean": "rimraf dist",
|
25
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
26
|
+
"lint": "eslint src --ext .ts",
|
27
|
+
"typecheck": "tsc --noEmit",
|
28
|
+
"prepublishOnly": "npm run clean && npm run build"
|
29
|
+
},
|
30
|
+
"keywords": [
|
31
|
+
"crosspost",
|
32
|
+
"sdk",
|
33
|
+
"api",
|
34
|
+
"social-media"
|
35
|
+
],
|
36
|
+
"author": "crosspost.near",
|
37
|
+
"license": "MIT",
|
38
|
+
"dependencies": {
|
39
|
+
"@types/js-cookie": "^3.0.6",
|
40
|
+
"js-cookie": "^3.0.5",
|
41
|
+
"near-sign-verify": "^0.1.1"
|
42
|
+
},
|
43
|
+
"devDependencies": {
|
44
|
+
"@types/node": "^20.10.5",
|
45
|
+
"eslint": "^8.56.0",
|
46
|
+
"rimraf": "^5.0.5",
|
47
|
+
"tsup": "^8.0.1",
|
48
|
+
"typescript": "^5.3.3"
|
49
|
+
},
|
50
|
+
"publishConfig": {
|
51
|
+
"access": "public"
|
52
|
+
}
|
53
|
+
}
|
package/src/api/auth.ts
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
import type {
|
2
|
+
AuthRevokeResponse,
|
3
|
+
AuthStatusResponse,
|
4
|
+
ConnectedAccountsResponse,
|
5
|
+
EnhancedApiResponse,
|
6
|
+
NearAuthorizationResponse,
|
7
|
+
Platform,
|
8
|
+
} from '@crosspost/types';
|
9
|
+
import { makeRequest, type RequestOptions } from '../core/request.ts';
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Authentication-related API operations
|
13
|
+
*/
|
14
|
+
export class AuthApi {
|
15
|
+
private options: RequestOptions;
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Creates an instance of AuthApi
|
19
|
+
* @param options Request options
|
20
|
+
*/
|
21
|
+
constructor(options: RequestOptions) {
|
22
|
+
this.options = options;
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Authorizes the NEAR account associated with the provided nearAuthData with the Crosspost service.
|
27
|
+
* @returns A promise resolving with the authorization response.
|
28
|
+
*/
|
29
|
+
async authorizeNearAccount(): Promise<NearAuthorizationResponse> {
|
30
|
+
return makeRequest<NearAuthorizationResponse>(
|
31
|
+
'POST',
|
32
|
+
'/auth/authorize/near',
|
33
|
+
this.options,
|
34
|
+
{},
|
35
|
+
);
|
36
|
+
}
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Checks the authorization status of the NEAR account with the Crosspost service.
|
40
|
+
* @returns A promise resolving with the authorization status response.
|
41
|
+
*/
|
42
|
+
async getNearAuthorizationStatus(): Promise<NearAuthorizationResponse> {
|
43
|
+
return makeRequest<NearAuthorizationResponse>(
|
44
|
+
'GET',
|
45
|
+
'/auth/authorize/near/status',
|
46
|
+
this.options,
|
47
|
+
);
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Initiates the login process for a specific platform.
|
52
|
+
* The service handles the OAuth flow; this method triggers it.
|
53
|
+
* @param platform The target platform.
|
54
|
+
* @param options Optional success and error redirect URLs.
|
55
|
+
* @returns A promise resolving with the response from the service (might indicate success/failure or redirect info).
|
56
|
+
*/
|
57
|
+
async loginToPlatform(
|
58
|
+
platform: Platform,
|
59
|
+
options?: { successUrl?: string; errorUrl?: string },
|
60
|
+
): Promise<EnhancedApiResponse<any>> { // TODO: Refine response type based on actual API
|
61
|
+
return makeRequest<EnhancedApiResponse<any>>(
|
62
|
+
'POST',
|
63
|
+
`/auth/${platform}/login`,
|
64
|
+
this.options,
|
65
|
+
options || {},
|
66
|
+
);
|
67
|
+
}
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Refreshes the authentication token for the specified platform.
|
71
|
+
* @param platform The target platform.
|
72
|
+
* @returns A promise resolving with the refresh response.
|
73
|
+
*/
|
74
|
+
async refreshToken(platform: Platform): Promise<EnhancedApiResponse<any>> { // TODO: Refine response type
|
75
|
+
return makeRequest<EnhancedApiResponse<any>>(
|
76
|
+
'POST',
|
77
|
+
`/auth/${platform}/refresh`,
|
78
|
+
this.options,
|
79
|
+
);
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Refreshes the user's profile information from the specified platform.
|
84
|
+
* @param platform The target platform.
|
85
|
+
* @returns A promise resolving with the profile refresh response.
|
86
|
+
*/
|
87
|
+
async refreshProfile(platform: Platform): Promise<EnhancedApiResponse<any>> { // TODO: Refine response type
|
88
|
+
return makeRequest<EnhancedApiResponse<any>>(
|
89
|
+
'POST',
|
90
|
+
`/auth/${platform}/refresh-profile`,
|
91
|
+
this.options,
|
92
|
+
);
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Gets the authentication status for the specified platform.
|
97
|
+
* @param platform The target platform.
|
98
|
+
* @returns A promise resolving with the authentication status response.
|
99
|
+
*/
|
100
|
+
async getAuthStatus(platform: Platform): Promise<AuthStatusResponse> {
|
101
|
+
return makeRequest<AuthStatusResponse>(
|
102
|
+
'GET',
|
103
|
+
`/auth/${platform}/status`,
|
104
|
+
this.options,
|
105
|
+
);
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Revokes the authentication token for the specified platform.
|
110
|
+
* @param platform The target platform.
|
111
|
+
* @returns A promise resolving with the revocation response.
|
112
|
+
*/
|
113
|
+
async revokeAuth(platform: Platform): Promise<AuthRevokeResponse> {
|
114
|
+
return makeRequest<AuthRevokeResponse>(
|
115
|
+
'DELETE',
|
116
|
+
`/auth/${platform}/revoke`,
|
117
|
+
this.options,
|
118
|
+
);
|
119
|
+
}
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Lists all accounts connected to the NEAR account.
|
123
|
+
* @returns A promise resolving with the list of connected accounts.
|
124
|
+
*/
|
125
|
+
async getConnectedAccounts(): Promise<ConnectedAccountsResponse> {
|
126
|
+
return makeRequest<ConnectedAccountsResponse>(
|
127
|
+
'GET',
|
128
|
+
'/auth/accounts',
|
129
|
+
this.options,
|
130
|
+
);
|
131
|
+
}
|
132
|
+
}
|
package/src/api/post.ts
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
import type {
|
2
|
+
CreatePostRequest,
|
3
|
+
CreatePostResponse,
|
4
|
+
DeletePostRequest,
|
5
|
+
DeletePostResponse,
|
6
|
+
LikePostRequest,
|
7
|
+
LikePostResponse,
|
8
|
+
QuotePostRequest,
|
9
|
+
QuotePostResponse,
|
10
|
+
ReplyToPostRequest,
|
11
|
+
ReplyToPostResponse,
|
12
|
+
RepostRequest,
|
13
|
+
RepostResponse,
|
14
|
+
UnlikePostRequest,
|
15
|
+
UnlikePostResponse,
|
16
|
+
} from '@crosspost/types';
|
17
|
+
import { ApiError, ApiErrorCode } from '@crosspost/types';
|
18
|
+
import { makeRequest, type RequestOptions } from '../core/request.ts';
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Post-related API operations
|
22
|
+
*/
|
23
|
+
export class PostApi {
|
24
|
+
private options: RequestOptions;
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Creates an instance of PostApi
|
28
|
+
* @param options Request options
|
29
|
+
*/
|
30
|
+
constructor(options: RequestOptions) {
|
31
|
+
this.options = options;
|
32
|
+
}
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Creates a new post on the specified target platforms.
|
36
|
+
* @param request The post creation request details.
|
37
|
+
* @returns A promise resolving with the post creation response.
|
38
|
+
*/
|
39
|
+
async createPost(request: CreatePostRequest): Promise<CreatePostResponse> {
|
40
|
+
return makeRequest<CreatePostResponse>(
|
41
|
+
'POST',
|
42
|
+
'/api/post',
|
43
|
+
this.options,
|
44
|
+
request,
|
45
|
+
);
|
46
|
+
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Reposts an existing post on the specified target platforms.
|
50
|
+
* @param request The repost request details.
|
51
|
+
* @returns A promise resolving with the repost response.
|
52
|
+
*/
|
53
|
+
async repost(request: RepostRequest): Promise<RepostResponse> {
|
54
|
+
return makeRequest<RepostResponse>(
|
55
|
+
'POST',
|
56
|
+
'/api/post/repost',
|
57
|
+
this.options,
|
58
|
+
request,
|
59
|
+
);
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Quotes an existing post on the specified target platforms.
|
64
|
+
* @param request The quote post request details.
|
65
|
+
* @returns A promise resolving with the quote post response.
|
66
|
+
*/
|
67
|
+
async quotePost(request: QuotePostRequest): Promise<QuotePostResponse> {
|
68
|
+
return makeRequest<QuotePostResponse>(
|
69
|
+
'POST',
|
70
|
+
'/api/post/quote',
|
71
|
+
this.options,
|
72
|
+
request,
|
73
|
+
);
|
74
|
+
}
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Replies to an existing post on the specified target platforms.
|
78
|
+
* @param request The reply request details.
|
79
|
+
* @returns A promise resolving with the reply response.
|
80
|
+
*/
|
81
|
+
async replyToPost(request: ReplyToPostRequest): Promise<ReplyToPostResponse> {
|
82
|
+
return makeRequest<ReplyToPostResponse>(
|
83
|
+
'POST',
|
84
|
+
'/api/post/reply',
|
85
|
+
this.options,
|
86
|
+
request,
|
87
|
+
);
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Likes a post on the specified target platforms.
|
92
|
+
* @param request The like request details.
|
93
|
+
* @returns A promise resolving with the like response.
|
94
|
+
*/
|
95
|
+
async likePost(request: LikePostRequest): Promise<LikePostResponse> {
|
96
|
+
// API endpoint uses postId in the path
|
97
|
+
return makeRequest<LikePostResponse>(
|
98
|
+
'POST',
|
99
|
+
`/api/post/like/${request.postId}`,
|
100
|
+
this.options,
|
101
|
+
request,
|
102
|
+
);
|
103
|
+
}
|
104
|
+
|
105
|
+
/**
|
106
|
+
* Unlikes a post on the specified target platforms.
|
107
|
+
* @param request The unlike request details.
|
108
|
+
* @returns A promise resolving with the unlike response.
|
109
|
+
*/
|
110
|
+
async unlikePost(request: UnlikePostRequest): Promise<UnlikePostResponse> {
|
111
|
+
// API endpoint uses postId in the path
|
112
|
+
return makeRequest<UnlikePostResponse>(
|
113
|
+
'DELETE',
|
114
|
+
`/api/post/like/${request.postId}`,
|
115
|
+
this.options,
|
116
|
+
request,
|
117
|
+
);
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
* Deletes one or more posts.
|
122
|
+
* @param request The delete request details.
|
123
|
+
* @returns A promise resolving with the delete response.
|
124
|
+
*/
|
125
|
+
async deletePost(request: DeletePostRequest): Promise<DeletePostResponse> {
|
126
|
+
// API endpoint uses postId in the path, assuming the first post ID for the URL
|
127
|
+
const postId = request.posts[0]?.postId || '';
|
128
|
+
if (!postId) {
|
129
|
+
throw new ApiError(
|
130
|
+
'Post ID is required for deletion path',
|
131
|
+
ApiErrorCode.VALIDATION_ERROR,
|
132
|
+
400,
|
133
|
+
);
|
134
|
+
}
|
135
|
+
return makeRequest<DeletePostResponse>(
|
136
|
+
'DELETE',
|
137
|
+
`/api/post/${postId}`,
|
138
|
+
this.options,
|
139
|
+
request,
|
140
|
+
);
|
141
|
+
}
|
142
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import type { NearAuthData as NearSignatureData } from 'near-sign-verify';
|
2
|
+
import { AuthApi } from '../api/auth.ts';
|
3
|
+
import { PostApi } from '../api/post.ts';
|
4
|
+
import { type CrosspostClientConfig, DEFAULT_CONFIG } from './config.ts';
|
5
|
+
import type { RequestOptions } from './request.ts';
|
6
|
+
import { getAuthFromCookie, storeAuthInCookie } from '../utils/cookie.ts';
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Main client for interacting with the Crosspost API service.
|
10
|
+
*/
|
11
|
+
export class CrosspostClient {
|
12
|
+
/**
|
13
|
+
* Authentication-related API operations
|
14
|
+
*/
|
15
|
+
public readonly auth: AuthApi;
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Post-related API operations
|
19
|
+
*/
|
20
|
+
public readonly post: PostApi;
|
21
|
+
|
22
|
+
private readonly options: RequestOptions;
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Creates an instance of CrosspostClient.
|
26
|
+
* @param config Configuration options for the client.
|
27
|
+
*/
|
28
|
+
constructor(config: CrosspostClientConfig = {}) {
|
29
|
+
const baseUrl = config.baseUrl || DEFAULT_CONFIG.baseUrl; // you can deploy your own
|
30
|
+
const timeout = config.timeout || DEFAULT_CONFIG.timeout;
|
31
|
+
const retries = config.retries ?? DEFAULT_CONFIG.retries;
|
32
|
+
|
33
|
+
// Try to get auth data from config or cookie
|
34
|
+
const signature = config.signature || getAuthFromCookie();
|
35
|
+
|
36
|
+
this.options = {
|
37
|
+
baseUrl,
|
38
|
+
timeout,
|
39
|
+
retries,
|
40
|
+
signature,
|
41
|
+
};
|
42
|
+
|
43
|
+
this.auth = new AuthApi(this.options);
|
44
|
+
this.post = new PostApi(this.options);
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Sets the authentication data (signature) for the client and stores it in a cookie
|
49
|
+
* @param signature The NEAR authentication data
|
50
|
+
*/
|
51
|
+
public async setAuthentication(signature: NearSignatureData): Promise<void> {
|
52
|
+
// Update the client's auth data
|
53
|
+
this.options.signature = signature;
|
54
|
+
|
55
|
+
// Store in cookie for persistence
|
56
|
+
storeAuthInCookie(signature);
|
57
|
+
}
|
58
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import type { NearAuthData as NearSignatureData } from 'near-sign-verify';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Configuration options for the CrosspostClient
|
5
|
+
*/
|
6
|
+
export interface CrosspostClientConfig {
|
7
|
+
/**
|
8
|
+
* Base URL for the Crosspost API
|
9
|
+
* @default 'https://api.opencrosspost.com'
|
10
|
+
*/
|
11
|
+
baseUrl?: string;
|
12
|
+
/**
|
13
|
+
* NEAR authentication data obtained from near-sign-verify
|
14
|
+
*/
|
15
|
+
signature?: NearSignatureData;
|
16
|
+
/**
|
17
|
+
* Request timeout in milliseconds
|
18
|
+
* @default 30000
|
19
|
+
*/
|
20
|
+
timeout?: number;
|
21
|
+
/**
|
22
|
+
* Number of retries for failed requests (specifically for network errors or 5xx status codes)
|
23
|
+
* @default 2
|
24
|
+
*/
|
25
|
+
retries?: number;
|
26
|
+
}
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Default configuration values for the CrosspostClient
|
30
|
+
*/
|
31
|
+
export const DEFAULT_CONFIG: Required<Omit<CrosspostClientConfig, 'signature'>> = {
|
32
|
+
baseUrl: 'https://open-crosspost-proxy.deno.dev/',
|
33
|
+
timeout: 30000,
|
34
|
+
retries: 2,
|
35
|
+
};
|
@@ -0,0 +1,158 @@
|
|
1
|
+
import { ApiError, ApiErrorCode } from '@crosspost/types';
|
2
|
+
import { createAuthToken, type NearAuthData as NearSignatureData } from 'near-sign-verify';
|
3
|
+
import { createNetworkError, handleErrorResponse } from '../utils/error.ts';
|
4
|
+
import { CSRF_HEADER_NAME, getCsrfToken } from '../utils/cookie.ts';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Options for making a request to the API
|
8
|
+
*/
|
9
|
+
export interface RequestOptions {
|
10
|
+
/**
|
11
|
+
* Base URL for the API
|
12
|
+
*/
|
13
|
+
baseUrl: string;
|
14
|
+
/**
|
15
|
+
* NEAR authentication data for generating auth tokens
|
16
|
+
* Can be undefined if not authorize yet
|
17
|
+
*/
|
18
|
+
signature?: NearSignatureData;
|
19
|
+
/**
|
20
|
+
* Request timeout in milliseconds
|
21
|
+
*/
|
22
|
+
timeout: number;
|
23
|
+
/**
|
24
|
+
* Number of retries for failed requests
|
25
|
+
*/
|
26
|
+
retries: number;
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Makes a request to the API with retry and error handling
|
31
|
+
*
|
32
|
+
* @param method The HTTP method
|
33
|
+
* @param path The API path
|
34
|
+
* @param options The request options
|
35
|
+
* @param data Optional request data
|
36
|
+
* @returns A promise resolving with the response data
|
37
|
+
*/
|
38
|
+
export async function makeRequest<T>(
|
39
|
+
method: string,
|
40
|
+
path: string,
|
41
|
+
options: RequestOptions,
|
42
|
+
data?: any,
|
43
|
+
): Promise<T> {
|
44
|
+
const url = `${options.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
45
|
+
let lastError: Error | null = null;
|
46
|
+
|
47
|
+
// Check if authentication data is available
|
48
|
+
if (!options.signature) {
|
49
|
+
throw ApiError.unauthorized('Authentication required. Please provide NEAR signature.');
|
50
|
+
}
|
51
|
+
|
52
|
+
for (let attempt = 0; attempt <= options.retries; attempt++) {
|
53
|
+
const controller = new AbortController();
|
54
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
55
|
+
|
56
|
+
try {
|
57
|
+
const headers: Record<string, string> = {
|
58
|
+
'Content-Type': 'application/json',
|
59
|
+
'Accept': 'application/json',
|
60
|
+
'Authorization': `Bearer ${createAuthToken(options.signature)}`,
|
61
|
+
};
|
62
|
+
|
63
|
+
// Add CSRF token for state-changing requests (non-GET)
|
64
|
+
if (method !== 'GET') {
|
65
|
+
const csrfToken = getCsrfToken();
|
66
|
+
if (csrfToken) {
|
67
|
+
headers[CSRF_HEADER_NAME] = csrfToken;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
const requestOptions: RequestInit = {
|
72
|
+
method,
|
73
|
+
headers,
|
74
|
+
body: method !== 'GET' && data ? JSON.stringify(data) : undefined,
|
75
|
+
signal: controller.signal,
|
76
|
+
};
|
77
|
+
|
78
|
+
const response = await fetch(url, requestOptions);
|
79
|
+
clearTimeout(timeoutId); // Clear timeout if fetch completes
|
80
|
+
|
81
|
+
let responseData: any;
|
82
|
+
try {
|
83
|
+
responseData = await response.json();
|
84
|
+
} catch (jsonError) {
|
85
|
+
// If JSON parsing fails, throw a specific error or handle based on status
|
86
|
+
if (!response.ok) {
|
87
|
+
throw new ApiError(
|
88
|
+
`API request failed with status ${response.status} and non-JSON response`,
|
89
|
+
ApiErrorCode.NETWORK_ERROR, // Or a more specific code
|
90
|
+
response.status as any,
|
91
|
+
{ originalStatusText: response.statusText },
|
92
|
+
);
|
93
|
+
}
|
94
|
+
// If response was ok but JSON failed, maybe it was an empty 204 response?
|
95
|
+
if (response.status === 204) return {} as T; // Handle No Content
|
96
|
+
// Otherwise, rethrow JSON parse error or a custom error
|
97
|
+
throw new ApiError(
|
98
|
+
`Failed to parse JSON response: ${
|
99
|
+
jsonError instanceof Error ? jsonError.message : String(jsonError)
|
100
|
+
}`,
|
101
|
+
ApiErrorCode.INTERNAL_ERROR, // Or NETWORK_ERROR?
|
102
|
+
response.status as any,
|
103
|
+
);
|
104
|
+
}
|
105
|
+
|
106
|
+
if (!response.ok) {
|
107
|
+
lastError = handleErrorResponse(responseData, response.status);
|
108
|
+
// Retry only on 5xx errors or potentially recoverable errors if defined
|
109
|
+
const shouldRetry = response.status >= 500 ||
|
110
|
+
(lastError instanceof ApiError && lastError.recoverable);
|
111
|
+
if (shouldRetry && attempt < options.retries) {
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
|
113
|
+
continue; // Retry
|
114
|
+
}
|
115
|
+
throw lastError; // Throw error if not retrying or retries exhausted
|
116
|
+
}
|
117
|
+
|
118
|
+
// Handle cases where API indicates failure within a 2xx response
|
119
|
+
if (
|
120
|
+
responseData && typeof responseData === 'object' && 'success' in responseData &&
|
121
|
+
!responseData.success && responseData.error
|
122
|
+
) {
|
123
|
+
lastError = handleErrorResponse(responseData, response.status);
|
124
|
+
// Decide if this specific type of "successful" response with an error payload should be retried
|
125
|
+
const shouldRetry = lastError instanceof ApiError && lastError.recoverable;
|
126
|
+
if (shouldRetry && attempt < options.retries) {
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
|
128
|
+
continue; // Retry
|
129
|
+
}
|
130
|
+
throw lastError;
|
131
|
+
}
|
132
|
+
|
133
|
+
return responseData as T; // Success
|
134
|
+
} catch (error) {
|
135
|
+
clearTimeout(timeoutId); // Clear timeout on error
|
136
|
+
lastError = error as Error; // Store the error
|
137
|
+
|
138
|
+
// Handle fetch/network errors specifically for retries
|
139
|
+
const isNetworkError = error instanceof TypeError ||
|
140
|
+
(error instanceof DOMException && error.name === 'AbortError');
|
141
|
+
if (isNetworkError && attempt < options.retries) {
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
|
143
|
+
continue; // Retry network error
|
144
|
+
}
|
145
|
+
|
146
|
+
// If it's not a known ApiError/PlatformError, wrap it
|
147
|
+
if (!(error instanceof ApiError)) {
|
148
|
+
throw createNetworkError(error, url, options.timeout);
|
149
|
+
}
|
150
|
+
|
151
|
+
throw error; // Re-throw known ApiError or final network error
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
// Should not be reachable if retries >= 0, but needed for type safety
|
156
|
+
throw lastError ||
|
157
|
+
new ApiError('Request failed after multiple retries', ApiErrorCode.INTERNAL_ERROR, 500);
|
158
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
/**
|
2
|
+
* @crosspost/sdk
|
3
|
+
* SDK for interacting with the Crosspost API
|
4
|
+
*/
|
5
|
+
|
6
|
+
// Export main client
|
7
|
+
export { CrosspostClient } from './core/client.js';
|
8
|
+
export { CrosspostClientConfig } from './core/config.js';
|
9
|
+
|
10
|
+
// Export API modules for advanced usage
|
11
|
+
export { AuthApi } from './api/auth.js';
|
12
|
+
export { PostApi } from './api/post.js';
|
13
|
+
|
14
|
+
// Export utility functions
|
15
|
+
export { createNetworkError, handleErrorResponse } from './utils/error.js';
|
16
|
+
export {
|
17
|
+
AUTH_COOKIE_NAME,
|
18
|
+
AUTH_COOKIE_OPTIONS,
|
19
|
+
clearAuthCookie,
|
20
|
+
CSRF_COOKIE_NAME,
|
21
|
+
CSRF_HEADER_NAME,
|
22
|
+
getAuthFromCookie,
|
23
|
+
getCsrfToken,
|
24
|
+
storeAuthInCookie,
|
25
|
+
} from './utils/cookie.js';
|
26
|
+
|
27
|
+
// Re-export types from @crosspost/types for convenience
|
28
|
+
export * from '@crosspost/types';
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import Cookies from 'js-cookie';
|
2
|
+
import type { NearAuthData } from 'near-sign-verify';
|
3
|
+
|
4
|
+
export const AUTH_COOKIE_NAME = '__crosspost_auth';
|
5
|
+
export const CSRF_COOKIE_NAME = 'XSRF-TOKEN';
|
6
|
+
export const CSRF_HEADER_NAME = 'X-CSRF-Token';
|
7
|
+
|
8
|
+
export const AUTH_COOKIE_OPTIONS: Cookies.CookieAttributes = {
|
9
|
+
secure: true,
|
10
|
+
sameSite: 'lax', // how could we make this none?
|
11
|
+
path: '/',
|
12
|
+
expires: 30, // 30 days
|
13
|
+
};
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Gets authentication data from the cookie
|
17
|
+
* @returns The NearAuthData object or undefined if not found
|
18
|
+
*/
|
19
|
+
export function getAuthFromCookie(): NearAuthData | undefined {
|
20
|
+
try {
|
21
|
+
if (typeof document === 'undefined') {
|
22
|
+
return undefined;
|
23
|
+
}
|
24
|
+
|
25
|
+
const cookieValue = Cookies.get(AUTH_COOKIE_NAME);
|
26
|
+
if (!cookieValue) {
|
27
|
+
return undefined;
|
28
|
+
}
|
29
|
+
|
30
|
+
return JSON.parse(cookieValue) as NearAuthData;
|
31
|
+
} catch (error) {
|
32
|
+
console.error('Failed to parse auth cookie:', error);
|
33
|
+
return undefined;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Stores authentication data in a secure cookie
|
39
|
+
* @param authData The NearAuthData object to store
|
40
|
+
*/
|
41
|
+
export function storeAuthInCookie(authData: NearAuthData): void {
|
42
|
+
try {
|
43
|
+
if (typeof document === 'undefined') {
|
44
|
+
return;
|
45
|
+
}
|
46
|
+
|
47
|
+
const cookieValue = JSON.stringify(authData);
|
48
|
+
Cookies.set(AUTH_COOKIE_NAME, cookieValue, AUTH_COOKIE_OPTIONS);
|
49
|
+
} catch (error) {
|
50
|
+
console.error('Failed to store auth cookie:', error);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
/**
|
55
|
+
* Clears the authentication cookie
|
56
|
+
*/
|
57
|
+
export function clearAuthCookie(): void {
|
58
|
+
if (typeof document === 'undefined') {
|
59
|
+
return;
|
60
|
+
}
|
61
|
+
|
62
|
+
Cookies.remove(AUTH_COOKIE_NAME, { path: AUTH_COOKIE_OPTIONS.path });
|
63
|
+
}
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Gets the CSRF token from the cookie
|
67
|
+
* @returns The CSRF token or undefined if not found
|
68
|
+
*/
|
69
|
+
export function getCsrfToken(): string | undefined {
|
70
|
+
if (typeof document === 'undefined') {
|
71
|
+
return undefined;
|
72
|
+
}
|
73
|
+
|
74
|
+
return Cookies.get(CSRF_COOKIE_NAME);
|
75
|
+
}
|