@crosspost/sdk 0.1.2 → 0.1.3

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.
@@ -0,0 +1,77 @@
1
+ import type {
2
+ AccountActivityQuery,
3
+ AccountActivityResponse,
4
+ AccountPostsQuery,
5
+ AccountPostsResponse,
6
+ ActivityLeaderboardQuery,
7
+ ActivityLeaderboardResponse,
8
+ } from '@crosspost/types';
9
+ import { makeRequest, type RequestOptions } from '../core/request.ts';
10
+
11
+ /**
12
+ * Activity-related API operations
13
+ */
14
+ export class ActivityApi {
15
+ private options: RequestOptions;
16
+
17
+ /**
18
+ * Creates an instance of ActivityApi
19
+ * @param options Request options
20
+ */
21
+ constructor(options: RequestOptions) {
22
+ this.options = options;
23
+ }
24
+
25
+ /**
26
+ * Gets the global activity leaderboard
27
+ * @param query Optional query parameters
28
+ * @returns A promise resolving with the leaderboard response
29
+ */
30
+ async getLeaderboard(query?: ActivityLeaderboardQuery): Promise<ActivityLeaderboardResponse> {
31
+ return makeRequest<ActivityLeaderboardResponse>(
32
+ 'GET',
33
+ '/api/activity',
34
+ this.options,
35
+ undefined,
36
+ query,
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Gets activity for a specific account
42
+ * @param signerId The NEAR account ID
43
+ * @param query Optional query parameters
44
+ * @returns A promise resolving with the account activity response
45
+ */
46
+ async getAccountActivity(
47
+ signerId: string,
48
+ query?: AccountActivityQuery,
49
+ ): Promise<AccountActivityResponse> {
50
+ return makeRequest<AccountActivityResponse>(
51
+ 'GET',
52
+ `/api/activity/${signerId}`,
53
+ this.options,
54
+ undefined,
55
+ query,
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Gets posts for a specific account
61
+ * @param signerId The NEAR account ID
62
+ * @param query Optional query parameters
63
+ * @returns A promise resolving with the account posts response
64
+ */
65
+ async getAccountPosts(
66
+ signerId: string,
67
+ query?: AccountPostsQuery,
68
+ ): Promise<AccountPostsResponse> {
69
+ return makeRequest<AccountPostsResponse>(
70
+ 'GET',
71
+ `/api/activity/${signerId}/posts`,
72
+ this.options,
73
+ undefined,
74
+ query,
75
+ );
76
+ }
77
+ }
@@ -0,0 +1,55 @@
1
+ import type { EndpointRateLimitResponse, RateLimitResponse } from '@crosspost/types';
2
+ import { makeRequest, type RequestOptions } from '../core/request.ts';
3
+
4
+ /**
5
+ * System-related API operations
6
+ * Includes rate limits, health checks, and other system-related functionality
7
+ */
8
+ export class SystemApi {
9
+ private options: RequestOptions;
10
+
11
+ /**
12
+ * Creates an instance of SystemApi
13
+ * @param options Request options
14
+ */
15
+ constructor(options: RequestOptions) {
16
+ this.options = options;
17
+ }
18
+
19
+ /**
20
+ * Gets the current rate limit status
21
+ * @returns A promise resolving with the rate limit response
22
+ */
23
+ async getRateLimits(): Promise<RateLimitResponse> {
24
+ return makeRequest<RateLimitResponse>(
25
+ 'GET',
26
+ '/api/rate-limit',
27
+ this.options,
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Gets the rate limit status for a specific endpoint
33
+ * @param endpoint The endpoint to get rate limit for
34
+ * @returns A promise resolving with the endpoint rate limit response
35
+ */
36
+ async getEndpointRateLimit(endpoint: string): Promise<EndpointRateLimitResponse> {
37
+ return makeRequest<EndpointRateLimitResponse>(
38
+ 'GET',
39
+ `/api/rate-limit/${endpoint}`,
40
+ this.options,
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Gets the health status of the API
46
+ * @returns A promise resolving with the health status
47
+ */
48
+ async getHealthStatus(): Promise<{ status: string }> {
49
+ return makeRequest<{ status: string }>(
50
+ 'GET',
51
+ '/health',
52
+ this.options,
53
+ );
54
+ }
55
+ }
@@ -1,6 +1,8 @@
1
- import type { NearAuthData as NearSignatureData } from 'near-sign-verify';
1
+ import type { NearAuthData } from 'near-sign-verify';
2
+ import { ActivityApi } from '../api/activity.ts';
2
3
  import { AuthApi } from '../api/auth.ts';
3
4
  import { PostApi } from '../api/post.ts';
5
+ import { SystemApi } from '../api/system.ts';
4
6
  import { type CrosspostClientConfig, DEFAULT_CONFIG } from './config.ts';
5
7
  import type { RequestOptions } from './request.ts';
6
8
  import { getAuthFromCookie, storeAuthInCookie } from '../utils/cookie.ts';
@@ -9,15 +11,10 @@ import { getAuthFromCookie, storeAuthInCookie } from '../utils/cookie.ts';
9
11
  * Main client for interacting with the Crosspost API service.
10
12
  */
11
13
  export class CrosspostClient {
12
- /**
13
- * Authentication-related API operations
14
- */
15
14
  public readonly auth: AuthApi;
16
-
17
- /**
18
- * Post-related API operations
19
- */
20
15
  public readonly post: PostApi;
16
+ public readonly activity: ActivityApi;
17
+ public readonly system: SystemApi;
21
18
 
22
19
  private readonly options: RequestOptions;
23
20
 
@@ -30,29 +27,35 @@ export class CrosspostClient {
30
27
  const timeout = config.timeout || DEFAULT_CONFIG.timeout;
31
28
  const retries = config.retries ?? DEFAULT_CONFIG.retries;
32
29
 
33
- // Try to get auth data from config or cookie
34
- const signature = config.signature || getAuthFromCookie();
30
+ const nearAuthData = config.nearAuthData || getAuthFromCookie();
35
31
 
36
32
  this.options = {
37
33
  baseUrl,
38
34
  timeout,
39
35
  retries,
40
- signature,
36
+ nearAuthData,
41
37
  };
42
38
 
43
39
  this.auth = new AuthApi(this.options);
44
40
  this.post = new PostApi(this.options);
41
+ this.activity = new ActivityApi(this.options);
42
+ this.system = new SystemApi(this.options);
45
43
  }
46
44
 
47
45
  /**
48
46
  * Sets the authentication data (signature) for the client and stores it in a cookie
49
47
  * @param signature The NEAR authentication data
50
48
  */
51
- public async setAuthentication(signature: NearSignatureData): Promise<void> {
52
- // Update the client's auth data
53
- this.options.signature = signature;
49
+ public setAuthentication(nearAuthData: NearAuthData): void {
50
+ this.options.nearAuthData = nearAuthData;
51
+ storeAuthInCookie(nearAuthData);
52
+ }
54
53
 
55
- // Store in cookie for persistence
56
- storeAuthInCookie(signature);
54
+ /**
55
+ * Checks if authentication data (signature) exists on client
56
+ * @param signature The NEAR authentication data
57
+ */
58
+ public isAuthenticated(): boolean {
59
+ return !!this.options.nearAuthData;
57
60
  }
58
61
  }
@@ -1,4 +1,4 @@
1
- import type { NearAuthData as NearSignatureData } from 'near-sign-verify';
1
+ import type { NearAuthData } from 'near-sign-verify';
2
2
 
3
3
  /**
4
4
  * Configuration options for the CrosspostClient
@@ -6,13 +6,13 @@ import type { NearAuthData as NearSignatureData } from 'near-sign-verify';
6
6
  export interface CrosspostClientConfig {
7
7
  /**
8
8
  * Base URL for the Crosspost API
9
- * @default 'https://api.opencrosspost.com'
9
+ * @default 'https://open-crosspost-proxy.deno.dev'
10
10
  */
11
11
  baseUrl?: string;
12
12
  /**
13
13
  * NEAR authentication data obtained from near-sign-verify
14
14
  */
15
- signature?: NearSignatureData;
15
+ nearAuthData?: NearAuthData;
16
16
  /**
17
17
  * Request timeout in milliseconds
18
18
  * @default 30000
@@ -28,7 +28,7 @@ export interface CrosspostClientConfig {
28
28
  /**
29
29
  * Default configuration values for the CrosspostClient
30
30
  */
31
- export const DEFAULT_CONFIG: Required<Omit<CrosspostClientConfig, 'signature'>> = {
31
+ export const DEFAULT_CONFIG: Required<Omit<CrosspostClientConfig, 'nearAuthData'>> = {
32
32
  baseUrl: 'https://open-crosspost-proxy.deno.dev/',
33
33
  timeout: 30000,
34
34
  retries: 2,
@@ -1,6 +1,7 @@
1
1
  import { ApiError, ApiErrorCode } from '@crosspost/types';
2
- import { createAuthToken, type NearAuthData as NearSignatureData } from 'near-sign-verify';
2
+ import { createAuthToken, type NearAuthData } from 'near-sign-verify';
3
3
  import { createNetworkError, handleErrorResponse } from '../utils/error.ts';
4
+ import { apiWrapper, enrichErrorWithContext } from '../utils/error-utils.ts';
4
5
  import { CSRF_HEADER_NAME, getCsrfToken } from '../utils/cookie.ts';
5
6
 
6
7
  /**
@@ -15,7 +16,7 @@ export interface RequestOptions {
15
16
  * NEAR authentication data for generating auth tokens
16
17
  * Can be undefined if not authorize yet
17
18
  */
18
- signature?: NearSignatureData;
19
+ nearAuthData?: NearAuthData;
19
20
  /**
20
21
  * Request timeout in milliseconds
21
22
  */
@@ -40,119 +41,144 @@ export async function makeRequest<T>(
40
41
  path: string,
41
42
  options: RequestOptions,
42
43
  data?: any,
44
+ query?: Record<string, any>,
43
45
  ): Promise<T> {
44
- const url = `${options.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
45
- let lastError: Error | null = null;
46
-
46
+ let url = `${options.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
47
+
48
+ // Add query parameters if provided
49
+ if (query && Object.keys(query).length > 0) {
50
+ const queryParams = new URLSearchParams();
51
+ for (const [key, value] of Object.entries(query)) {
52
+ if (value !== undefined && value !== null) {
53
+ queryParams.append(key, String(value));
54
+ }
55
+ }
56
+ const queryString = queryParams.toString();
57
+ if (queryString) {
58
+ url += `?${queryString}`;
59
+ }
60
+ }
47
61
  // Check if authentication data is available
48
- if (!options.signature) {
62
+ if (!options.nearAuthData) {
49
63
  throw ApiError.unauthorized('Authentication required. Please provide NEAR signature.');
50
64
  }
51
65
 
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
- }
66
+ // Create a context object for error enrichment
67
+ const context = {
68
+ method,
69
+ path,
70
+ url,
71
+ retries: options.retries,
72
+ };
70
73
 
71
- const requestOptions: RequestInit = {
72
- method,
73
- headers,
74
- body: method !== 'GET' && data ? JSON.stringify(data) : undefined,
75
- signal: controller.signal,
76
- };
74
+ return apiWrapper(async () => {
75
+ let lastError: Error | null = null;
77
76
 
78
- const response = await fetch(url, requestOptions);
79
- clearTimeout(timeoutId); // Clear timeout if fetch completes
77
+ for (let attempt = 0; attempt <= options.retries; attempt++) {
78
+ const controller = new AbortController();
79
+ const timeoutId = setTimeout(() => controller.abort(), options.timeout);
80
80
 
81
- let responseData: any;
82
81
  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) {
82
+ const headers: Record<string, string> = {
83
+ 'Content-Type': 'application/json',
84
+ 'Accept': 'application/json',
85
+ 'Authorization': `Bearer ${createAuthToken(options.nearAuthData!)}`,
86
+ };
87
+
88
+ // Add CSRF token for state-changing requests (non-GET)
89
+ if (method !== 'GET') {
90
+ const csrfToken = getCsrfToken();
91
+ if (csrfToken) {
92
+ headers[CSRF_HEADER_NAME] = csrfToken;
93
+ }
94
+ }
95
+
96
+ const requestOptions: RequestInit = {
97
+ method,
98
+ headers,
99
+ body: method !== 'GET' && data ? JSON.stringify(data) : undefined,
100
+ signal: controller.signal,
101
+ };
102
+
103
+ const response = await fetch(url, requestOptions);
104
+ clearTimeout(timeoutId); // Clear timeout if fetch completes
105
+
106
+ let responseData: any;
107
+ try {
108
+ responseData = await response.json();
109
+ } catch (jsonError) {
110
+ // If JSON parsing fails, throw a specific error or handle based on status
111
+ if (!response.ok) {
112
+ throw new ApiError(
113
+ `API request failed with status ${response.status} and non-JSON response`,
114
+ ApiErrorCode.NETWORK_ERROR,
115
+ response.status as any,
116
+ { originalStatusText: response.statusText },
117
+ );
118
+ }
119
+ // If response was ok but JSON failed, maybe it was an empty 204 response?
120
+ if (response.status === 204) return {} as T; // Handle No Content
121
+ // Otherwise, throw a custom error
87
122
  throw new ApiError(
88
- `API request failed with status ${response.status} and non-JSON response`,
89
- ApiErrorCode.NETWORK_ERROR, // Or a more specific code
123
+ `Failed to parse JSON response: ${
124
+ jsonError instanceof Error ? jsonError.message : String(jsonError)
125
+ }`,
126
+ ApiErrorCode.INTERNAL_ERROR,
90
127
  response.status as any,
91
- { originalStatusText: response.statusText },
92
128
  );
93
129
  }
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
130
 
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
131
+ if (!response.ok) {
132
+ lastError = handleErrorResponse(responseData, response.status);
133
+ // Retry only on 5xx errors or potentially recoverable errors if defined
134
+ const shouldRetry = response.status >= 500 ||
135
+ (lastError instanceof ApiError && lastError.recoverable);
136
+ if (shouldRetry && attempt < options.retries) {
137
+ await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
138
+ continue; // Retry
139
+ }
140
+ throw lastError; // Throw error if not retrying or retries exhausted
114
141
  }
115
- throw lastError; // Throw error if not retrying or retries exhausted
116
- }
117
142
 
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) {
143
+ // Handle cases where API indicates failure within a 2xx response
144
+ if (
145
+ responseData && typeof responseData === 'object' && 'success' in responseData &&
146
+ !responseData.success && responseData.error
147
+ ) {
148
+ lastError = handleErrorResponse(responseData, response.status);
149
+ // Decide if this specific type of "successful" response with an error payload should be retried
150
+ const shouldRetry = lastError instanceof ApiError && lastError.recoverable;
151
+ if (shouldRetry && attempt < options.retries) {
152
+ await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
153
+ continue; // Retry
154
+ }
155
+ throw lastError;
156
+ }
157
+
158
+ return responseData as T;
159
+ } catch (error) {
160
+ clearTimeout(timeoutId); // Clear timeout on error
161
+ lastError = error instanceof Error ? error : new Error(String(error)); // Store the error
162
+
163
+ // Handle fetch/network errors specifically for retries
164
+ const isNetworkError = error instanceof TypeError ||
165
+ (error instanceof DOMException && error.name === 'AbortError');
166
+ if (isNetworkError && attempt < options.retries) {
127
167
  await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); // Exponential backoff
128
- continue; // Retry
168
+ continue; // Retry network error
129
169
  }
130
- throw lastError;
131
- }
132
170
 
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
- }
171
+ // If it's not a known ApiError/PlatformError, wrap it
172
+ if (!(error instanceof ApiError)) {
173
+ throw createNetworkError(error, url, options.timeout);
174
+ }
145
175
 
146
- // If it's not a known ApiError/PlatformError, wrap it
147
- if (!(error instanceof ApiError)) {
148
- throw createNetworkError(error, url, options.timeout);
176
+ throw error; // Re-throw known ApiError or final network error
149
177
  }
150
-
151
- throw error; // Re-throw known ApiError or final network error
152
178
  }
153
- }
154
179
 
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);
180
+ // Should not be reachable if retries >= 0, but needed for type safety
181
+ throw lastError ||
182
+ new ApiError('Request failed after multiple retries', ApiErrorCode.INTERNAL_ERROR, 500);
183
+ }, context);
158
184
  }
package/src/index.ts CHANGED
@@ -4,15 +4,34 @@
4
4
  */
5
5
 
6
6
  // Export main client
7
- export { CrosspostClient } from './core/client.js';
8
- export { CrosspostClientConfig } from './core/config.js';
7
+ export { CrosspostClient } from './core/client.ts';
8
+ export type { CrosspostClientConfig } from './core/config.ts';
9
9
 
10
10
  // Export API modules for advanced usage
11
- export { AuthApi } from './api/auth.js';
12
- export { PostApi } from './api/post.js';
11
+ export { ActivityApi } from './api/activity.ts';
12
+ export { AuthApi } from './api/auth.ts';
13
+ export { PostApi } from './api/post.ts';
14
+ export { SystemApi } from './api/system.ts';
13
15
 
14
16
  // Export utility functions
15
- export { createNetworkError, handleErrorResponse } from './utils/error.js';
17
+ export { createNetworkError, handleErrorResponse } from './utils/error.ts';
18
+ export {
19
+ apiWrapper,
20
+ enrichErrorWithContext,
21
+ ERROR_CATEGORIES,
22
+ getErrorDetails,
23
+ getErrorMessage,
24
+ isAuthError,
25
+ isContentError,
26
+ isErrorOfCategory,
27
+ isMediaError,
28
+ isNetworkError,
29
+ isPlatformError,
30
+ isPostError,
31
+ isRateLimitError,
32
+ isRecoverableError,
33
+ isValidationError,
34
+ } from './utils/error-utils.ts';
16
35
  export {
17
36
  AUTH_COOKIE_NAME,
18
37
  AUTH_COOKIE_OPTIONS,
@@ -22,7 +41,7 @@ export {
22
41
  getAuthFromCookie,
23
42
  getCsrfToken,
24
43
  storeAuthInCookie,
25
- } from './utils/cookie.js';
44
+ } from './utils/cookie.ts';
26
45
 
27
46
  // Re-export types from @crosspost/types for convenience
28
47
  export * from '@crosspost/types';
@@ -5,9 +5,25 @@ export const AUTH_COOKIE_NAME = '__crosspost_auth';
5
5
  export const CSRF_COOKIE_NAME = 'XSRF-TOKEN';
6
6
  export const CSRF_HEADER_NAME = 'X-CSRF-Token';
7
7
 
8
+ /**
9
+ * Checks if the code is running in a Deno environment
10
+ */
11
+ export const isDeno = (): boolean => { // monorepo builds primarily for Deno
12
+ // we could expect that frontends in Deno environment will use this package,
13
+ // and then we can determine auth solution there (ValTown, Smallweb, etc)
14
+ return typeof (globalThis as any).Deno !== 'undefined';
15
+ };
16
+
17
+ /**
18
+ * Checks if the code is running in a browser environment
19
+ */
20
+ export const isBrowser = (): boolean => {
21
+ return !isDeno() && typeof globalThis.window !== 'undefined';
22
+ };
23
+
8
24
  export const AUTH_COOKIE_OPTIONS: Cookies.CookieAttributes = {
9
25
  secure: true,
10
- sameSite: 'lax', // how could we make this none?
26
+ sameSite: 'lax', // Restrict to same-site and top-level navigation
11
27
  path: '/',
12
28
  expires: 30, // 30 days
13
29
  };
@@ -18,7 +34,7 @@ export const AUTH_COOKIE_OPTIONS: Cookies.CookieAttributes = {
18
34
  */
19
35
  export function getAuthFromCookie(): NearAuthData | undefined {
20
36
  try {
21
- if (typeof document === 'undefined') {
37
+ if (!isBrowser()) {
22
38
  return undefined;
23
39
  }
24
40
 
@@ -40,7 +56,7 @@ export function getAuthFromCookie(): NearAuthData | undefined {
40
56
  */
41
57
  export function storeAuthInCookie(authData: NearAuthData): void {
42
58
  try {
43
- if (typeof document === 'undefined') {
59
+ if (!isBrowser()) {
44
60
  return;
45
61
  }
46
62
 
@@ -55,7 +71,7 @@ export function storeAuthInCookie(authData: NearAuthData): void {
55
71
  * Clears the authentication cookie
56
72
  */
57
73
  export function clearAuthCookie(): void {
58
- if (typeof document === 'undefined') {
74
+ if (!isBrowser()) {
59
75
  return;
60
76
  }
61
77
 
@@ -67,7 +83,7 @@ export function clearAuthCookie(): void {
67
83
  * @returns The CSRF token or undefined if not found
68
84
  */
69
85
  export function getCsrfToken(): string | undefined {
70
- if (typeof document === 'undefined') {
86
+ if (!isBrowser()) {
71
87
  return undefined;
72
88
  }
73
89