UNPKG

@crosspost/sdk

Version:

SDK for interacting with the Crosspost API

856 lines (844 loc) 24.5 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ActivityApi: () => ActivityApi, AuthApi: () => AuthApi, CrosspostClient: () => CrosspostClient, CrosspostError: () => CrosspostError, PostApi: () => PostApi, SystemApi: () => SystemApi, getErrorDetails: () => getErrorDetails, getErrorMessage: () => getErrorMessage, isAuthError: () => isAuthError, isContentError: () => isContentError, isMediaError: () => isMediaError, isNetworkError: () => isNetworkError, isPlatformError: () => isPlatformError, isPostError: () => isPostError, isRateLimitError: () => isRateLimitError, isRecoverableError: () => isRecoverableError, isValidationError: () => isValidationError }); module.exports = __toCommonJS(index_exports); // src/core/request.ts var import_async_retry = __toESM(require("async-retry"), 1); var import_types2 = require("@crosspost/types"); // src/utils/error.ts var import_types = require("@crosspost/types"); var CrosspostError = class extends Error { constructor(message, code, status, details, recoverable = false) { super(message); this.name = "CrosspostError"; this.code = code; this.status = status; this.details = details; this.recoverable = recoverable; } /** * Get platform from details if available */ get platform() { return this.details?.platform; } /** * Get userId from details if available */ get userId() { return this.details?.userId; } }; function isErrorCode(error, codes) { return error instanceof CrosspostError && codes.includes(error.code); } function isAuthError(error) { return isErrorCode(error, [ import_types.ApiErrorCode.UNAUTHORIZED, import_types.ApiErrorCode.FORBIDDEN ]); } function isValidationError(error) { return isErrorCode(error, [ import_types.ApiErrorCode.VALIDATION_ERROR, import_types.ApiErrorCode.INVALID_REQUEST ]); } function isNetworkError(error) { return isErrorCode(error, [ import_types.ApiErrorCode.NETWORK_ERROR, import_types.ApiErrorCode.PLATFORM_UNAVAILABLE ]); } function isPlatformError(error) { return error instanceof CrosspostError && !!error.details?.platform; } function isContentError(error) { return isErrorCode(error, [ import_types.ApiErrorCode.CONTENT_POLICY_VIOLATION, import_types.ApiErrorCode.DUPLICATE_CONTENT ]); } function isRateLimitError(error) { return isErrorCode(error, [import_types.ApiErrorCode.RATE_LIMITED]); } function isPostError(error) { return isErrorCode(error, [ import_types.ApiErrorCode.POST_CREATION_FAILED, import_types.ApiErrorCode.THREAD_CREATION_FAILED, import_types.ApiErrorCode.POST_DELETION_FAILED, import_types.ApiErrorCode.POST_INTERACTION_FAILED ]); } function isMediaError(error) { return isErrorCode(error, [import_types.ApiErrorCode.MEDIA_UPLOAD_FAILED]); } function isRecoverableError(error) { return error instanceof CrosspostError && error.recoverable; } function getErrorMessage(error, defaultMessage = "An error occurred") { if (error instanceof Error) { return error.message || defaultMessage; } return defaultMessage; } function getErrorDetails(error) { if (error instanceof CrosspostError) { return error.details; } return void 0; } function createError(message, code, status, details, recoverable = false) { return new CrosspostError( message, code, status, details, recoverable ); } function enrichErrorWithContext(error, context) { if (error instanceof CrosspostError) { return createError( error.message, error.code, error.status, { ...error.details || {}, ...context }, error.recoverable ); } const errorMessage = error instanceof Error ? error.message : String(error); return createError( errorMessage || "An error occurred", import_types.ApiErrorCode.INTERNAL_ERROR, 500, { originalError: error, ...context } ); } function handleErrorResponse(data, status) { if (!data || typeof data !== "object" || !("success" in data)) { return createError( "Invalid API response format", import_types.ApiErrorCode.INTERNAL_ERROR, status, { originalResponse: data } ); } if (!data.errors || !Array.isArray(data.errors) || data.errors.length === 0) { return createError( "Invalid error response format", import_types.ApiErrorCode.INTERNAL_ERROR, status, { originalResponse: data } ); } if (data.errors.length === 1) { const errorDetail = data.errors[0]; if (!errorDetail.message || !errorDetail.code) { return createError( "Invalid error detail format", import_types.ApiErrorCode.INTERNAL_ERROR, status, { originalResponse: data } ); } const finalDetails = { ...errorDetail.details || {}, ...data.meta || {} }; return createError( errorDetail.message, errorDetail.code, status, finalDetails, errorDetail.recoverable ?? false ); } else { const firstError = data.errors[0]; return createError( "Multiple errors occurred", firstError.code, status, { errors: data.errors, ...data.meta || {}, originalResponse: data }, false ); } } function createNetworkError(error, url, timeout) { if (error instanceof DOMException && error.name === "AbortError") { return createError( `Request timed out after ${timeout}ms`, import_types.ApiErrorCode.NETWORK_ERROR, 408, { url } ); } return createError( error instanceof Error ? error.message : "An unexpected error occurred during the request", import_types.ApiErrorCode.INTERNAL_ERROR, 500, { originalError: String(error), url } ); } // src/core/request.ts async function makeRequest(method, path, options, data, query) { const url = new URL(path, options.baseUrl); if (query && typeof query === "object" && Object.keys(query).length > 0) { for (const [key, value] of Object.entries(query)) { if (value !== void 0 && value !== null) { url.searchParams.append(key, String(value)); } } } const context = { method, path, url }; return (0, import_async_retry.default)( async (bail, _attemptNumber) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), options.timeout); try { const headers = { "Content-Type": "application/json", "Accept": "application/json" }; if (method === "GET") { const accountId = options.accountId; if (!accountId) { throw new CrosspostError( "No NEAR account provided for GET request", import_types2.ApiErrorCode.UNAUTHORIZED, 401 ); } headers["X-Near-Account"] = accountId; } else { if (!options.authToken) { throw new CrosspostError( "Auth token required for non-GET request", import_types2.ApiErrorCode.UNAUTHORIZED, 401 ); } headers["Authorization"] = `Bearer ${options.authToken}`; } const requestOptions = { method, headers, body: method !== "GET" && data ? JSON.stringify(data) : void 0, signal: controller.signal }; const response = await fetch(url, requestOptions); clearTimeout(timeoutId); let responseData; try { responseData = await response.json(); } catch (jsonError) { let responseText; try { responseText = await response.text(); } catch (_) { } throw new CrosspostError( `API request failed with status ${response.status} and non-JSON response`, import_types2.ApiErrorCode.INVALID_RESPONSE, response.status, { originalStatusText: response.statusText, originalError: jsonError instanceof Error ? jsonError.message : String(jsonError), responseText } ); } if (!response.ok) { throw handleErrorResponse(responseData, response.status); } if (!responseData || typeof responseData !== "object" || !("success" in responseData) || !("meta" in responseData)) { throw new CrosspostError( "Invalid response format from API", import_types2.ApiErrorCode.INVALID_RESPONSE, response.status, { responseData } ); } if (responseData.success) { return responseData; } throw handleErrorResponse(responseData, response.status); } catch (error) { clearTimeout(timeoutId); if (error instanceof TypeError || error instanceof DOMException && error.name === "AbortError") { throw error; } if (error instanceof CrosspostError) { const enrichedError = enrichErrorWithContext(error, context); bail(enrichedError); throw enrichedError; } if (error instanceof TypeError || error instanceof DOMException && error.name === "AbortError") { const networkError = createNetworkError(error, url.toString(), options.timeout); const enrichedNetworkError = enrichErrorWithContext(networkError, context); bail(enrichedNetworkError); throw enrichedNetworkError; } const wrappedError = new CrosspostError( error instanceof Error ? error.message : String(error), import_types2.ApiErrorCode.INTERNAL_ERROR, 500, { originalError: String(error) } ); const enrichedWrappedError = enrichErrorWithContext(wrappedError, context); bail(enrichedWrappedError); throw enrichedWrappedError; } }, { retries: 3, factor: 1, minTimeout: 1e3, maxTimeout: 1e3, onRetry: (error, attempt) => { console.warn( `Attempt ${attempt} failed for ${context.method} ${context.path}: ${error instanceof Error ? error.message : "Unknown error"}. Retrying...` ); } } ); } // src/api/activity.ts var ActivityApi = class { /** * Creates an instance of ActivityApi * @param options Request options */ constructor(options) { this.options = options; } /** * Gets the global activity leaderboard * @param query Optional query parameters * @returns A promise resolving with the leaderboard response */ async getLeaderboard(query) { return makeRequest( "GET", "/api/activity", this.options, void 0, query ); } /** * Gets activity for a specific account * @param signerId The NEAR account ID * @param query Optional query parameters * @returns A promise resolving with the account activity response */ async getAccountActivity(signerId, query) { return makeRequest( "GET", `/api/activity/${signerId}`, this.options, void 0, query ); } /** * Gets posts for a specific account * @param signerId The NEAR account ID * @param query Optional query parameters * @returns A promise resolving with the account posts response */ async getAccountPosts(signerId, query) { return makeRequest( "GET", `/api/activity/${signerId}/posts`, this.options, void 0, query ); } }; // src/utils/popup.ts function openAuthPopup(url, options = {}) { if (typeof window === "undefined") { throw new Error("openAuthPopup can only be used in a browser environment"); } return new Promise((resolve, reject) => { const { width = 600, height = 700, left = Math.max(0, (window.innerWidth - 600) / 2), top = Math.max(0, (window.innerHeight - 700) / 2) } = options; const popup = window.open( url, "authPopup", `width=${width},height=${height},left=${left},top=${top},scrollbars=yes` ); if (!popup) { reject(new Error("Popup blocked. Please allow popups for this site.")); return; } let messageReceived = false; const handleMessage = (event) => { if (!popup || event.source !== popup) { return; } const message = event.data; if (message?.type === "AUTH_CALLBACK") { messageReceived = true; window.removeEventListener("message", handleMessage); clearInterval(checkClosedInterval); if (message.data.success) { resolve(message.data); } else { reject(message.data); } } }; window.addEventListener("message", handleMessage); const checkClosedInterval = setInterval(() => { try { if (!popup || popup.closed) { cleanup(); } } catch (e) { console.warn("Error checking popup state:", e); cleanup(); } }, 500); function cleanup() { clearInterval(checkClosedInterval); window.removeEventListener("message", handleMessage); if (!messageReceived) { reject({ success: false, error: "Authentication cancelled by user.", status: { message: "Authentication Cancelled", code: "AUTH_CANCELLED", details: "The authentication window was closed before completion." } }); } } }); } // src/api/auth.ts var AuthApi = class { /** * Creates an instance of AuthApi * @param options Request options */ constructor(options) { this.options = options; } /** * Authorizes the NEAR account associated with the provided authToken with the Crosspost service. * @returns A promise resolving with the authorization response. */ async authorizeNearAccount() { return makeRequest( "POST", "/auth/authorize/near", this.options, {} ); } /** * Checks the authorization status of the NEAR account with the Crosspost service. * @returns A promise resolving with the authorization status response. */ async getNearAuthorizationStatus() { return makeRequest( "GET", "/auth/authorize/near/status", this.options ); } /** * Initiates the login process for a specific platform using a popup window. * @param platform The target platform. * @param options Optional success and error redirect URLs. * @returns Promise that resolves with the authentication result when the popup completes. * @throws Error if popups are blocked or if running in a non-browser environment. */ async loginToPlatform(platform, options) { const requestOptions = options || { redirect: false }; const response = await makeRequest( "POST", `/auth/${platform}/login`, this.options, requestOptions ); if (requestOptions.redirect) { return response; } if (!response.data || !("url" in response.data)) { throw new Error("Invalid authentication URL response"); } const result = await openAuthPopup(response.data.url); if (!result.success || !result.userId) { throw new Error(result.error || "Authentication failed"); } return { platform, userId: result.userId, status: result.status }; } /** * Refreshes the authentication token for the specified platform. * @param platform The target platform. * @returns A promise resolving with the refresh response containing updated auth details. */ async refreshToken(platform, userId) { return makeRequest( "POST", `/auth/${platform}/refresh`, this.options, { userId } ); } /** * Refreshes the user's profile information from the specified platform. * @param platform The target platform. * @param userId The user ID on the platform * @returns A promise resolving with the updated account profile information. */ async refreshProfile(platform, userId) { return makeRequest( "POST", `/auth/${platform}/refresh-profile`, this.options, { userId } ); } /** * Gets the authentication status for the specified platform. * @param platform The target platform. * @returns A promise resolving with the authentication status response. */ async getAuthStatus(platform, userId) { return makeRequest( "GET", `/auth/${platform}/status/${userId}`, this.options, void 0, { platform, userId } ); } /** * Unauthorizes a NEAR account from using the service * @returns A promise resolving with the unauthorized response */ async unauthorizeNear() { return makeRequest( "DELETE", "/auth/unauthorize/near", this.options, {} ); } /** * Revokes the authentication token for the specified platform. * @param platform The target platform. * @returns A promise resolving with the revocation response. */ async revokeAuth(platform, userId) { return makeRequest( "DELETE", `/auth/${platform}/revoke`, this.options, { userId } ); } /** * Lists all accounts connected to the NEAR account. * @returns A promise resolving with the connected accounts response containing an array of accounts. * @throws {CrosspostError} If the request fails or returns invalid data. */ async getConnectedAccounts() { return makeRequest( "GET", "/auth/accounts", this.options ); } }; // src/api/post.ts var PostApi = class { /** * Creates an instance of PostApi * @param options Request options */ constructor(options) { this.options = options; } /** * Creates a new post on the specified target platforms. * @param request The post creation request details. * @returns A promise resolving with the post creation response. */ async createPost(request) { return makeRequest( "POST", "/api/post", this.options, request ); } /** * Reposts an existing post on the specified target platforms. * @param request The repost request details. * @returns A promise resolving with the repost response. */ async repost(request) { return makeRequest( "POST", "/api/post/repost", this.options, request ); } /** * Quotes an existing post on the specified target platforms. * @param request The quote post request details. * @returns A promise resolving with the quote post response. */ async quotePost(request) { return makeRequest( "POST", "/api/post/quote", this.options, request ); } /** * Replies to an existing post on the specified target platforms. * @param request The reply request details. * @returns A promise resolving with the reply response. */ async replyToPost(request) { return makeRequest( "POST", "/api/post/reply", this.options, request ); } /** * Likes a post on the specified target platforms. * @param request The like request details. * @returns A promise resolving with the like response. */ async likePost(request) { return makeRequest( "POST", `/api/post/like`, this.options, request ); } /** * Unlikes a post on the specified target platforms. * @param request The unlike request details. * @returns A promise resolving with the unlike response. */ async unlikePost(request) { return makeRequest( "DELETE", `/api/post/like`, this.options, request ); } /** * Deletes one or more posts. * @param request The delete request details. * @returns A promise resolving with the delete response. */ async deletePost(request) { return makeRequest( "DELETE", `/api/post`, this.options, request ); } }; // src/api/system.ts var SystemApi = class { /** * Creates an instance of SystemApi * @param options Request options */ constructor(options) { this.options = options; } /** * Gets the current rate limit status * @returns A promise resolving with the rate limit response */ async getRateLimits() { return makeRequest( "GET", "/api/rate-limit", this.options ); } /** * Gets the rate limit status for a specific endpoint * @param endpoint The endpoint to get rate limit for * @returns A promise resolving with the endpoint rate limit response */ async getEndpointRateLimit(endpoint) { return makeRequest( "GET", `/api/rate-limit/${endpoint}`, this.options, void 0, { endpoint } ); } /** * Gets the health status of the API * @returns A promise resolving with the health status */ async getHealthStatus() { return makeRequest( "GET", "/health", this.options ); } }; // src/core/config.ts var DEFAULT_CONFIG = { baseUrl: new URL("https://api.opencrosspost.com/"), timeout: 3e4 }; // src/core/client.ts var CrosspostClient = class { /** * Creates an instance of CrosspostClient. * @param config Configuration options for the client. */ constructor(config = {}) { const baseUrl = config.baseUrl || DEFAULT_CONFIG.baseUrl; const timeout = config.timeout || DEFAULT_CONFIG.timeout; const authToken = config.authToken; this.options = { baseUrl: baseUrl instanceof URL ? baseUrl : new URL(baseUrl), timeout, authToken }; this.auth = new AuthApi(this.options); this.post = new PostApi(this.options); this.activity = new ActivityApi(this.options); this.system = new SystemApi(this.options); } /** * Sets the authentication data (signature) for the client * Required for non-GET requests * @param authToken The NEAR authentication data */ setAuthentication(authToken) { this.options.authToken = authToken; } /** * Sets the NEAR account ID for simplified GET request authentication * @param accountId The NEAR account ID */ setAccountHeader(accountId) { this.options.accountId = accountId; } /** * Checks if authentication data (signature) exists on client * @returns true if authToken is set (required for non-GET requests) */ isAuthenticated() { return !!this.options.authToken; } /** * Clears all authentication data from the client * This will prevent all requests from working until new authentication is set */ clear() { this.options.authToken = void 0; this.options.accountId = void 0; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ActivityApi, AuthApi, CrosspostClient, CrosspostError, PostApi, SystemApi, getErrorDetails, getErrorMessage, isAuthError, isContentError, isMediaError, isNetworkError, isPlatformError, isPostError, isRateLimitError, isRecoverableError, isValidationError });