npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@author-today-tools/api

v0.1.1

Published

TypeScript API wrapper for Author.Today platform

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/api

Documentation

📚 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 API

Authentication

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 changes

Security 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 needed

How 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:

  1. 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,
});
  1. 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());
});
  1. 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:

  1. Environment Variables (Best for Servers):
const client = new AuthorTodayClient({
  token: process.env.AUTHOR_TODAY_TOKEN,
});
  1. 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(() => {});
    }
  },
};
  1. 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:

  1. 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...');
    }
  },
});
  1. 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;
}
  1. 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