@crosspost/sdk
Version:
SDK for interacting with the Crosspost API
856 lines (844 loc) • 24.5 kB
JavaScript
;
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
});