@author-today-tools/api
v0.1.1
Published
TypeScript API wrapper for Author.Today platform
Maintainers
Readme
@author-today-tools/api
Isomorphic TypeScript API client for the Author.Today publishing platform. Works in both Node.js (24+) and modern browsers.
Installation
npm install @author-today-tools/api
# or
pnpm add @author-today-tools/api
# or
yarn add @author-today-tools/apiDocumentation
📚 Full API Reference - Complete TypeScript API documentation
Usage
import { AuthorTodayClient } from '@author-today-tools/api';
const client = new AuthorTodayClient({
baseUrl: 'https://api.author.today',
timeout: 30000,
});
// Use the client to interact with Author.Today APIAuthentication
The client supports Bearer token authentication for accessing protected resources.
Basic Authentication
import { AuthorTodayClient } from '@author-today-tools/api';
// Create client and set authentication token
const client = new AuthorTodayClient();
await client.setToken('your-bearer-token-here');
// Or initialize with token
const authenticatedClient = new AuthorTodayClient({
token: 'your-bearer-token-here',
});
// Check if authenticated
if (await client.isAuthenticated()) {
// Make authenticated requests
}
// Clear authentication
await client.setToken(null);Guest Token Support
For endpoints that require guest authentication:
await client.setGuestToken('guest-token-from-api');Token Persistence
⚠️ Security Warning: Token storage has important security implications. See the Security section below for best practices.
Implement token storage for persistence across sessions:
import { AuthorTodayClient, type TokenStorage } from '@author-today-tools/api';
// ⚠️ Browser: In-memory storage (Recommended for session-only)
// Most secure option - tokens are lost on page refresh
let inMemoryToken: string | null = null;
const inMemoryStorage: TokenStorage = {
get: () => inMemoryToken,
set: (token) => {
inMemoryToken = token;
},
};
// ⚠️ Browser: localStorage (Use with caution - vulnerable to XSS)
// Only use if you fully trust all scripts on your page
const browserStorage: TokenStorage = {
get: () => localStorage.getItem('auth_token'),
set: (token) => {
if (token) {
localStorage.setItem('auth_token', token);
} else {
localStorage.removeItem('auth_token');
}
},
};
// ✅ Node.js: File-based storage (Recommended)
const fileStorage: TokenStorage = {
get: async () => {
try {
return await fs.readFile('token.txt', 'utf8');
} catch {
return null;
}
},
set: async (token) => {
if (token) {
await fs.writeFile('token.txt', token, { mode: 0o600 }); // Restrict permissions
} else {
await fs.unlink('token.txt').catch(() => {});
}
},
};
const client = new AuthorTodayClient({
tokenStorage: inMemoryStorage, // Recommended for browsers
});
// Token will be automatically loaded from storage and persisted on changesSecurity Recommendations by Environment:
Browser:
- Best: In-memory storage (session-only, most secure)
- Better: httpOnly cookies set by your backend (requires server support)
- Good: IndexedDB with encryption (advanced use cases)
- ⚠️ Avoid: localStorage/sessionStorage (vulnerable to XSS attacks)
Node.js:
- Best: Environment variables (for servers)
- Better: Encrypted files with restricted permissions
- Good: System keychain (Keychain on macOS, Credential Manager on Windows)
Automatic Token Refresh
The client automatically refreshes expired tokens when it receives a 401 Unauthorized error, provided a refresh token is available. This eliminates the need for manual token refresh handling in most cases.
import { AuthorTodayClient } from '@author-today-tools/api';
const client = new AuthorTodayClient();
// Login - client stores both access token and refresh token
const { token, refreshToken } = await client.login({
email: '[email protected]',
password: 'password',
rememberMe: true,
});
// Later, when the token expires, the client automatically:
// 1. Detects 401 error
// 2. Calls refreshToken() with stored refresh token
// 3. Updates the access token
// 4. Retries the original request
const library = await client.getUserLibrary(); // ✨ Auto-refreshes if neededHow It Works
- When any API request receives a 401 error, the client checks if a refresh token is available
- If available, it automatically calls the refresh token endpoint to get a new access token
- The new tokens (both access and refresh) are stored internally
- The original request is retried with the new access token
- If the refresh fails, the original 401 error is thrown
Refresh Token Storage
Refresh tokens are automatically stored when you use authentication methods:
// Refresh token is stored from login
await client.login({ email, password, rememberMe: true });
// Refresh token is stored from registration
await client.register({ email, password, username, agreedToTerms: true });
// Refresh token is updated when manually refreshing
await client.refreshToken(existingRefreshToken);
// Refresh token is stored from bearer token request
await client.getBearerToken();Manual Token Refresh
You can still manually refresh tokens if needed:
try {
// Manually refresh the token
const { token, refreshToken } = await client.refreshToken('your-refresh-token');
console.log('Token refreshed successfully');
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Refresh token invalid or expired');
// Redirect to login page
}
}Error Handling with Auto-Refresh
Auto-refresh only triggers on 401 errors. If the refresh itself fails, the original error is thrown:
import { AuthenticationError } from '@author-today-tools/api';
try {
const library = await client.getUserLibrary();
} catch (error) {
if (error instanceof AuthenticationError) {
// This could mean:
// 1. No token was provided
// 2. No refresh token was available
// 3. Refresh token is invalid/expired
// Action: Redirect user to login page
console.error('Authentication failed:', error.message);
redirectToLogin();
}
}How It Works
- Authentication tokens are automatically injected as
Authorization: Bearer <token>headers - Tokens can be stored in memory (default) or persisted via custom storage
- The client lazy-loads tokens from storage when needed
- All authentication methods are async to support both sync and async storage
- Expired tokens are automatically refreshed when a 401 error is detected
API Methods
Authentication
// Login with credentials
const { token } = await client.login({
email: '[email protected]',
password: 'password',
provider: 'credentials', // optional: 'credentials' | 'vk' | 'facebook' | 'google' | 'yandex'
rememberMe: true,
});
// Get bearer token
const { token } = await client.getBearerToken();
// Refresh expired token
const { token } = await client.refreshToken(refreshToken);
// Register new account
const { token } = await client.register({
email: '[email protected]',
password: 'password',
username: 'username',
agreedToTerms: true,
});User
// Get user's library
const library = await client.getUserLibrary({ page: 1, limit: 20 });
// Get reading progress
const progress = await client.getReadingProgress();Works
// Get work details
const work = await client.getWork(workId);
// Get work content (chapters)
const content = await client.getWorkContent(workId);
// Download work
const download = await client.downloadWork(workId, 'epub'); // 'fb2' | 'epub' | 'pdf' | 'mobi'
// Like a work
const like = await client.likeWork(workId);
// Get work statistics
const stats = await client.getWorkStatistics(workId);Catalog
// Get all genres
const genres = await client.getGenres();
// Get work statuses
const statuses = await client.getWorkStatuses();
// Search works
const results = await client.search({
q: 'search query',
genre: 'fantasy',
ratingPeriod: 'month', // 'day' | 'week' | 'month' | 'year' | 'all'
page: 1,
limit: 20,
});Audiobooks
// Get audiobook details
const audiobook = await client.getAudiobook(workId);
// Get audiobook playback URL
const { url, expiresAt } = await client.getAudiobookUrl(workId);Notifications
// Get notifications
const notifications = await client.getNotifications({
grouped: true,
page: 1,
limit: 20,
});
// Mark notifications as read
await client.markNotificationsRead([1, 2, 3]);File Upload
// Upload image file
const imageFile = new File(['...'], 'image.jpg', { type: 'image/jpeg' });
const imageResult = await client.uploadFile(imageFile, 'image');
console.log(imageResult.fileId, imageResult.url, imageResult.expiresAt);
// Upload PDF file
const pdfFile = new File(['...'], 'document.pdf', { type: 'application/pdf' });
const pdfResult = await client.uploadFile(pdfFile, 'pdf');
// Works with Blob objects too
const blob = new Blob(['data'], { type: 'image/png' });
const result = await client.uploadFile(blob, 'image');Error Handling
The client provides type-safe error classes for different error scenarios. All errors extend the base ApiError class.
Error Types
import {
ApiError,
ValidationError,
AuthenticationError,
ForbiddenError,
NotFoundError,
RateLimitError,
ServerError,
NetworkError,
TimeoutError,
ResponseValidationError,
} from '@author-today-tools/api';Handling Validation Errors (400)
Validation errors occur when request data is invalid:
try {
await client.register({
email: 'invalid-email',
password: '123', // too short
username: '',
agreedToTerms: false,
});
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation failed:', error.message);
console.error('Error code:', error.code);
// Check specific field errors
if (error.hasFieldError('email')) {
console.error('Email errors:', error.getFieldErrors('email'));
}
// Get all invalid fields
if (error.invalidFields) {
for (const field of error.invalidFields) {
console.error(`${field.field}: ${field.message}`);
}
}
}
}Handling Authentication Errors (401)
Authentication errors indicate invalid or missing credentials. Note that the client automatically attempts to refresh expired tokens if a refresh token is available (see "Automatic Token Refresh" section).
If you receive an AuthenticationError, it means either:
- No authentication token was provided
- No refresh token is available for auto-refresh
- The refresh token itself is invalid or expired
- Auto-refresh was attempted but failed
import { AuthenticationError } from '@author-today-tools/api';
try {
await client.getUserLibrary();
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Authentication failed:', error.message);
// At this point, auto-refresh has already been attempted if possible
// Action: Redirect user to login page
redirectToLogin();
}
}If you want to handle token refresh manually (bypassing auto-refresh), you can use the refreshToken() method directly:
try {
const { token } = await client.refreshToken(storedRefreshToken);
console.log('Token refreshed successfully');
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Refresh token invalid, please log in again');
redirectToLogin();
}
}Handling Not Found Errors (404)
Not found errors occur when a resource doesn't exist:
try {
const work = await client.getWork(999999);
} catch (error) {
if (error instanceof NotFoundError) {
console.error('Work not found:', error.message);
console.error('Status code:', error.statusCode); // 404
console.error('Error code:', error.code);
}
}Handling Rate Limiting (429)
The API has a rate limit of 20 requests per minute. The client automatically retries with exponential backoff:
try {
const work = await client.getWork(123);
} catch (error) {
if (error instanceof RateLimitError) {
console.error('Rate limit exceeded:', error.message);
// Get retry-after value (in seconds)
const retryAfter = error.getRetryAfter();
if (retryAfter) {
console.error(`Retry after ${retryAfter} seconds`);
}
}
}You can also handle rate limiting proactively with a custom callback:
const client = new AuthorTodayClient({
onRateLimit: (event) => {
console.log(`Rate limited! Retrying in ${event.delayMs}ms`);
console.log(`Attempt ${event.attempt + 1}/${event.maxRetries}`);
if (event.retryAfter) {
console.log(`Server suggested retry after ${event.retryAfter}s`);
}
},
});Handling Server Errors (500+)
Server errors indicate problems on the API side:
try {
const work = await client.getWork(123);
} catch (error) {
if (error instanceof ServerError) {
console.error('Server error:', error.message);
console.error('Status code:', error.statusCode); // 500, 502, 503, 504
// Implement retry logic or fallback
await retryWithExponentialBackoff(() => client.getWork(123));
}
}Handling Network Errors
Network errors occur when the API can't be reached:
try {
const work = await client.getWork(123);
} catch (error) {
if (error instanceof NetworkError) {
console.error('Network error:', error.message);
console.error('Original error:', error.originalError);
// Check internet connection or API availability
}
}Handling Timeout Errors
Timeout errors occur when requests take too long:
const client = new AuthorTodayClient({
timeout: 5000, // 5 seconds
});
try {
const work = await client.getWork(123);
} catch (error) {
if (error instanceof TimeoutError) {
console.error('Request timed out:', error.message);
console.error('Timeout value:', error.timeout); // 5000ms
// Retry with longer timeout or show user message
}
}Comprehensive Error Handling
Handle all error types in a single try-catch:
import {
ApiError,
ValidationError,
AuthenticationError,
NotFoundError,
RateLimitError,
ServerError,
NetworkError,
TimeoutError,
} from '@author-today-tools/api';
try {
const work = await client.getWork(123);
console.log('Work loaded:', work.title);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid request:', error.invalidFields);
} else if (error instanceof AuthenticationError) {
console.error('Not authenticated, redirecting to login...');
} else if (error instanceof NotFoundError) {
console.error('Work not found');
} else if (error instanceof RateLimitError) {
console.error(`Rate limited, retry after ${error.getRetryAfter()}s`);
} else if (error instanceof ServerError) {
console.error('Server error, please try again later');
} else if (error instanceof NetworkError) {
console.error('Network error, check your connection');
} else if (error instanceof TimeoutError) {
console.error('Request timed out');
} else if (ApiError.isApiError(error)) {
console.error('API error:', error.code, error.message);
} else {
console.error('Unexpected error:', error);
}
}Security
Token Storage Security
Critical Security Considerations:
Authentication tokens provide full access to user accounts. Improper storage can lead to account compromise.
Browser Security
❌ localStorage and sessionStorage Vulnerabilities:
// ❌ DANGEROUS: Vulnerable to XSS attacks
const badStorage: TokenStorage = {
get: () => localStorage.getItem('token'),
set: (token) => token && localStorage.setItem('token', token),
};Why this is dangerous:
- Any third-party script on your page can access localStorage
- XSS attacks can steal tokens from localStorage
- Browser extensions can read localStorage
- Tokens persist after logout if not properly cleared
✅ Recommended Approaches:
- In-memory storage (Most Secure for SPAs):
// Session-only storage - tokens are lost on page refresh
let tokenCache: string | null = null;
const secureStorage: TokenStorage = {
get: () => tokenCache,
set: (token) => {
tokenCache = token;
},
};
const client = new AuthorTodayClient({
tokenStorage: secureStorage,
});- httpOnly Cookies (Best for Full-Stack Apps):
// Backend sets httpOnly cookie
// Frontend makes requests with credentials
const client = new AuthorTodayClient({
baseUrl: '/api/proxy', // Your backend proxy
// Token is handled via httpOnly cookies
// No token storage needed in frontend
});Your backend proxy:
// Backend (Express example)
app.post('/api/proxy/*', async (req, res) => {
const token = req.cookies.auth_token; // httpOnly cookie
const response = await fetch(`https://api.author.today${req.path}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
});
res.json(await response.json());
});- IndexedDB with Encryption (Advanced):
import { encrypt, decrypt } from 'your-crypto-lib';
const encryptedStorage: TokenStorage = {
get: async () => {
const db = await openIndexedDB();
const encrypted = await db.get('tokens', 'auth_token');
return encrypted ? decrypt(encrypted) : null;
},
set: async (token) => {
const db = await openIndexedDB();
if (token) {
const encrypted = encrypt(token);
await db.put('tokens', encrypted, 'auth_token');
} else {
await db.delete('tokens', 'auth_token');
}
},
};Node.js Security
✅ Recommended Approaches:
- Environment Variables (Best for Servers):
const client = new AuthorTodayClient({
token: process.env.AUTHOR_TODAY_TOKEN,
});- Encrypted Files (Good for CLI Tools):
import { readFile, writeFile, chmod } from 'node:fs/promises';
const secureFileStorage: TokenStorage = {
get: async () => {
try {
const encrypted = await readFile('.token', 'utf8');
return decrypt(encrypted); // Use proper encryption
} catch {
return null;
}
},
set: async (token) => {
if (token) {
const encrypted = encrypt(token);
await writeFile('.token', encrypted, { mode: 0o600 });
} else {
await unlink('.token').catch(() => {});
}
},
};- System Keychain (Best for Desktop Apps):
import keytar from 'keytar'; // Or platform-specific solution
const keychainStorage: TokenStorage = {
get: async () => {
return await keytar.getPassword('author-today', 'auth-token');
},
set: async (token) => {
if (token) {
await keytar.setPassword('author-today', 'auth-token', token);
} else {
await keytar.deletePassword('author-today', 'auth-token');
}
},
};CORS Configuration
When using the API client in browsers, proper CORS configuration is essential:
Option 1: Backend Proxy (Recommended)
Create a backend proxy to handle API requests:
// Frontend
const client = new AuthorTodayClient({
baseUrl: '/api/author-today', // Your backend proxy endpoint
});
// Backend (Express)
app.use('/api/author-today', createProxyMiddleware({
target: 'https://api.author.today',
changeOrigin: true,
pathRewrite: {
'^/api/author-today': '',
},
onProxyReq: (proxyReq, req) => {
// Add authentication from httpOnly cookie
const token = req.cookies.auth_token;
if (token) {
proxyReq.setHeader('Authorization', `Bearer ${token}`);
}
},
}));Benefits:
- Avoids CORS issues entirely
- Keeps tokens in httpOnly cookies
- Allows request/response transformation
- Better security overall
Option 2: CORS Headers (If You Control the API)
If you control the API server, configure proper CORS headers:
// API Server (Express)
app.use(cors({
origin: 'https://your-app.com', // Specific origin, not '*'
credentials: true, // Allow cookies
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));Frontend:
const client = new AuthorTodayClient({
baseUrl: 'https://api.author.today',
// Browser will send credentials automatically
});Rate Limiting Best Practices
The API has a rate limit of 20 requests per minute. Follow these practices:
- Monitor Rate Limit Events:
const client = new AuthorTodayClient({
onRateLimit: (event) => {
// Log to monitoring service
analytics.track('rate_limit_hit', {
endpoint: event.endpoint,
attempt: event.attempt,
retryAfter: event.retryAfter,
});
// Show user-friendly message
if (event.attempt === event.maxRetries - 1) {
notifyUser('API rate limit reached, please wait a moment...');
}
},
});- Implement Caching:
const cache = new Map<string, { data: any; expiresAt: number }>();
async function getCachedWork(workId: number) {
const cacheKey = `work_${workId}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() < cached.expiresAt) {
return cached.data;
}
const work = await client.getWork(workId);
cache.set(cacheKey, {
data: work,
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
});
return work;
}- Batch Requests:
// ❌ BAD: Sends 100 sequential requests
for (const id of workIds) {
await client.getWork(id);
}
// ✅ GOOD: Batch with delay
async function batchGetWorks(ids: number[], delayMs = 1000) {
const results = [];
for (let i = 0; i < ids.length; i += 10) {
const batch = ids.slice(i, i + 10);
const works = await Promise.all(batch.map(id => client.getWork(id)));
results.push(...works);
if (i + 10 < ids.length) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
return results;
}Input Validation
Always validate user input before sending to the API:
import { ValidationError } from '@author-today-tools/api';
// Email validation
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Sanitize and validate
function sanitizeString(input: string, maxLength = 1000): string {
return input.trim().slice(0, maxLength);
}
async function safeLogin(email: string, password: string) {
// Client-side validation
const cleanEmail = sanitizeString(email, 255);
if (!isValidEmail(cleanEmail)) {
throw new Error('Invalid email format');
}
if (password.length < 8) {
throw new Error('Password must be at least 8 characters');
}
try {
return await client.login({
email: cleanEmail,
password,
rememberMe: true,
});
} catch (error) {
if (error instanceof ValidationError) {
// Handle server-side validation errors
console.error('Server validation failed:', error.invalidFields);
}
throw error;
}
}Error Message Security
Never expose sensitive information in error messages:
try {
await client.getUserLibrary();
} catch (error) {
if (error instanceof AuthenticationError) {
// ❌ BAD: Exposes token
console.error('Auth failed with token:', await client.getToken());
showError('Authentication failed: ' + error.message);
// ✅ GOOD: Generic message
console.error('Authentication failed');
showError('Please log in again');
redirectToLogin();
}
}Content Security Policy
Configure a strict CSP for browser applications:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
connect-src 'self' https://api.author.today;
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';">XSS Protection
When displaying API data in browsers:
// ❌ DANGEROUS: XSS vulnerability
element.innerHTML = work.description;
// ✅ SAFE: Use textContent or framework escaping
element.textContent = work.description;
// ✅ SAFE: React automatically escapes
return <div>{work.description}</div>;
// ✅ SAFE: Use DOMPurify for rich content
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(work.description);Additional Security Resources
For comprehensive security guidelines, see SECURITY.md in the repository root.
Advanced Usage
Pagination
Handle paginated responses efficiently:
// Fetch all pages of user library
async function getAllLibraryWorks(client: AuthorTodayClient) {
const allWorks = [];
let page = 1;
const limit = 50;
while (true) {
const response = await client.getUserLibrary({ page, limit });
allWorks.push(...response.works);
// Check if we've reached the last page
if (!response.pagination.hasNextPage || response.works.length === 0) {
break;
}
page++;
}
return allWorks;
}
// Usage
const allWorks = await getAllLibraryWorks(client);
console.log(`Total works in library: ${allWorks.length}`);Custom Retry Configuration
Customize retry behavior for rate limiting:
const client = new AuthorTodayClient({
retry: {
maxRetries: 5, // Retry up to 5 times (default: 3)
initialDelayMs: 2000, // Start with 2s delay (default: 1000ms)
maxDelayMs: 60000, // Cap at 60s (default: 30000ms)
backoffMultiplier: 2, // Double delay each retry (default: 2)
},
onRateLimit: (event) => {
// Track rate limit events for analytics
analytics.track('rate_limit_hit', {
attempt: event.attempt,
delay: event.delayMs,
});
},
});Custom Fetch Implementation
Use a custom fetch implementation (e.g., for request interception):
import { AuthorTodayClient } from '@author-today-tools/api';
// Node.js with custom agent
import { Agent } from 'https';
const httpsAgent = new Agent({
keepAlive: true,
maxSockets: 10,
});
const customFetch: typeof fetch = (url, options) => {
return fetch(url, {
...options,
// @ts-expect-error - Node.js specific
agent: httpsAgent,
});
};
const client = new AuthorTodayClient({
fetch: customFetch,
});
// Or with a logging wrapper
const loggingFetch: typeof fetch = async (url, options) => {
console.log(`[API] ${options?.method || 'GET'} ${url}`);
const startTime = Date.now();
try {
const response = await fetch(url, options);
const duration = Date.now() - startTime;
console.log(`[API] ${response.status} ${url} (${duration}ms)`);
return response;
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[API] Error ${url} (${duration}ms):`, error);
throw error;
}
};
const client = new AuthorTodayClient({
fetch: loggingFetch,
});Browser Usage
Complete browser example with secure in-memory storage:
import { AuthorTodayClient, type TokenStorage } from '@author-today-tools/api';
// ✅ Secure in-memory token storage (recommended)
// Tokens are cleared on page refresh - best security for SPAs
let tokenCache: string | null = null;
const browserStorage: TokenStorage = {
get: () => tokenCache,
set: (token) => {
tokenCache = token;
},
};
// Create client
const client = new AuthorTodayClient({
tokenStorage: browserStorage,
timeout: 30000,
onRateLimit: (event) => {
// Show toast notification to user
showToast(`Rate limited, retrying in ${Math.ceil(event.delayMs / 1000)}s...`);
},
});
// Login
async function login(email: string, password: string) {
try {
const { token } = await client.login({ email, password, rememberMe: true });
console.log('Login successful!');
// Token is automatically stored in localStorage
} catch (error) {
if (error instanceof ValidationError) {
// Show field errors in form
for (const field of error.invalidFields || []) {
showFieldError(field.field, field.message);
}
} else if (error instanceof AuthenticationError) {
showError('Invalid email or password');
} else {
showError('Login failed, please try again');
}
}
}
// Fetch user data
async function loadUserLibrary() {
try {
const library = await client.getUserLibrary({ page: 1, limit: 20 });
displayWorks(library.works);
} catch (error) {
if (error instanceof AuthenticationError) {
// Redirect to login page
window.location.href = '/login';
} else {
showError('Failed to load library');
}
}
}Node.js Usage
Complete Node.js example with file-based token persistence:
import { AuthorTodayClient, type TokenStorage } from '@author-today-tools/api';
import { readFile, writeFile, unlink } from 'node:fs/promises';
import { join } from 'node:path';
// Token storage using file system
const TOKEN_FILE = join(process.cwd(), '.auth-token');
const fileStorage: TokenStorage = {
get: async () => {
try {
return await readFile(TOKEN_FILE, 'utf8');
} catch {
return null;
}
},
set: async (token) => {
if (token) {
await writeFile(TOKEN_FILE, token, 'utf8');
} else {
await unlink(TOKEN_FILE).catch(() => {});
}
},
};
// Create client
const client = new AuthorTodayClient({
tokenStorage: fileStorage,
retry: {
maxRetries: 5,
initialDelayMs: 2000,
},
onRateLimit: (event) => {
console.warn(`⚠️ Rate limited, waiting ${event.delayMs}ms (attempt ${event.attempt + 1}/${event.maxRetries})`);
},
});
// Example: Download work in multiple formats
async function downloadWork(workId: number, outputDir: string) {
const formats = ['epub', 'pdf', 'fb2', 'mobi'] as const;
for (const format of formats) {
try {
const download = await client.downloadWork(workId, format);
const outputPath = join(outputDir, `work-${workId}.${format}`);
await writeFile(outputPath, Buffer.from(download.data));
console.log(`✅ Downloaded ${format}: ${outputPath}`);
} catch (error) {
if (error instanceof NotFoundError) {
console.log(`⏭️ Skipping ${format}: not available`);
} else {
console.error(`❌ Failed to download ${format}:`, error);
}
}
}
}
// Example: Batch process notifications
async function markAllNotificationsRead() {
let page = 1;
let processedCount = 0;
while (true) {
const response = await client.getNotifications({ page, limit: 100 });
if (response.notifications.length === 0) {
break;
}
const unreadIds = response.notifications
.filter(n => !n.isRead)
.map(n => n.id);
if (unreadIds.length > 0) {
await client.markNotificationsRead(unreadIds);
processedCount += unreadIds.length;
console.log(`Marked ${unreadIds.length} notifications as read`);
}
if (!response.pagination.hasNextPage) {
break;
}
page++;
}
console.log(`Total processed: ${processedCount} notifications`);
}Features
- Isomorphic: Works in Node.js (24+) and all modern browsers
- Type-safe: Full TypeScript support with strict type definitions
- Authentication: Bearer token support with optional persistent storage
- Dual format: ESM (
.mjs) and CommonJS (.cjs) support - Standard APIs: Uses native Fetch API (no Node.js-specific dependencies)
- Runtime validation: Zod-based schema validation
- Configurable: Custom timeouts, headers, and fetch implementation
- Tree-shakeable: Optimized for modern bundlers
- Zero dependencies: Only runtime dependency is Zod
License
MIT © Artyom Zakharov
