Minimal TypeScript auth handler for web sessions, CSRF tokens, and cookie persistence — inspired by Rust's cekunit-client.
libts-csrfx-auth is a TypeScript‑first library that automates authentication against web applications protected by CSRF tokens. It handles the complete two‑step login flow (GET login page → extract token → POST credentials), persists session data (cookies + token) to disk, and supports logout with automatic cache cleanup.
# Bun (recommended – fastest)
bun add libts-csrfx-auth
# npm
npm install libts-csrfx-auth
# pnpm
pnpm add libts-csrfx-auth
# yarn
yarn add libts-csrfx-auth
Requirements:
fetch and AbortController)Create a .env file in your project root:
BASE_URL=https://example.com
LOGIN_PATH=login
LOGOUT_PATH=logout
USER_EMAIL=admin@example.com
USER_PASSWORD=secret
Then:
import { AuthClient, LogoutClient } from 'libts-csrfx-auth'
const auth = new AuthClient()
const session = await auth.login()
console.log(`Logged in! CSRF token: ${session.csrfToken}`)
// Reuse cached session on next run
if (await auth.hasValidSession()) {
const cached = await auth.getCachedSession()
console.log(`Session from ${new Date(cached.timestamp)}`)
}
// Logout
const logout = new LogoutClient()
await logout.logout()
The library uses loadEnv() to read variables from .env (project root) and then falls back to Bun.env / process.env. All variables are validated.
| Variable | Required | Default | Validation |
|---|---|---|---|
BASE_URL |
– | Must be http:// or https:// |
|
LOGIN_PATH |
login |
No ? or # |
|
LOGOUT_PATH |
logout |
No ? or # |
|
USER_EMAIL |
– | Basic email regex | |
USER_PASSWORD |
– | Cannot be empty |
AuthClientrequires both email and password to be non‑empty (throwsINVALID_CREDENTIALSotherwise).
Example .env:
BASE_URL=https://staging.example.com
LOGIN_PATH=api/login
USER_EMAIL=ci@example.com
USER_PASSWORD=ci123
Most modern web frameworks (Laravel, Rails, Django, Symfony) protect login endpoints with a CSRF token that must be submitted along with credentials. The token is usually embedded in the login HTML page.
AuthClient automates this:
BASE_URL/LOGIN_PATH – extracts the CSRF token from the HTML (supports <input name="_token" value="..."> and <meta name="csrf-token" content="...">).Set-Cookie headers from the GET response (session cookie, etc.)._token, email, password, and the captured cookies.After a successful login, the session is saved as JSON to:
~/.cache/libts-csrfx-auth/session.json
The structure (SessionData):
interface SessionData {
cookies: Cookie[] // { name, value, domain, path, httpOnly, secure }
csrfToken: string // current CSRF token
loggedIn: boolean // always true after login
timestamp: number // Unix ms (Date.now())
}
On subsequent runs, hasValidSession(maxAgeMs) checks if the cached session is fresh (default 1 hour) and loggedIn === true. If valid, you can reuse it without re‑authenticating.
fetchWithRetry() wraps globalThis.fetch with:
AbortController; if exceeded, the attempt is aborted and counted as a failure.maxRetries attempts (default 3). Retryable conditions:
retryOn predicate.retryDelayMs * 2^attempt (e.g., 100ms, 200ms, 400ms).Both AuthClient and LogoutClient use this internally with sensible defaults.
AuthClientThe main client for logging in.
class AuthClient {
constructor(options?: LoginOptions)
login(): Promise<SessionData>
getCachedSession(): Promise<SessionData | null>
hasValidSession(maxAgeMs?: number): Promise<boolean>
clearSession(): Promise<void>
readonly cacheFilePath: string
readonly configRef: EnvConfig
}
LoginOptions)| Option | Type | Default | Description |
|---|---|---|---|
baseUrl |
string |
BASE_URL env |
Override base URL. |
email |
string |
USER_EMAIL env |
Override email. |
password |
string |
USER_PASSWORD env |
Override password. |
cacheDir |
string |
~/.cache/libts-csrfx-auth |
Custom directory for session cache. |
maxRetries |
number |
3 |
Max retry attempts (excluding initial try). |
retryDelayMs |
number |
100 |
Initial delay before first retry (exponential). |
timeoutMs |
number |
15000 |
Request timeout in ms. |
sendReferer |
boolean |
true |
Send Referer header in both GET and POST requests. |
sendOrigin |
boolean |
true |
Send Origin header in both requests. |
AuthClient Methodslogin(): Promise<SessionData>Performs the full two‑step login. Throws AuthError on failure. Saves session to disk.
getCachedSession(): Promise<SessionData | null>Returns the raw cached session (no freshness check). null if none exists or file corrupted.
hasValidSession(maxAgeMs = 3_600_000): Promise<boolean>Returns true if a cached session exists, is fresh (age < maxAgeMs), and loggedIn === true. Does not contact the server.
clearSession(): Promise<void>Deletes both the in‑memory cookie jar and the persistent cache file.
cacheFilePath: stringFull path to the session JSON file (e.g., /home/user/.cache/libts-csrfx-auth/session.json).
configRef: EnvConfigThe internal EnvConfig instance (read‑only) for advanced inspection.
LogoutClientTerminates an authenticated session.
class LogoutClient {
constructor(options?: LogoutOptions)
logout(): Promise<void>
logoutWithToken(csrfToken: string): Promise<void>
clearCache(): Promise<void>
loadCache(): Promise<SessionData | null>
readonly configRef: EnvConfig
}
LogoutOptions)| Option | Type | Default | Description |
|---|---|---|---|
baseUrl |
string |
BASE_URL env |
Override base URL. |
cacheDir |
string |
~/.cache/libts-csrfx-auth |
Custom cache directory. |
timeoutMs |
number |
15000 |
Request timeout in ms. |
sendReferer |
boolean |
true |
Send Referer header in POST. |
sendOrigin |
boolean |
true |
Send Origin header in POST. |
LogoutClient Methodslogout(): Promise<void>Loads the cached session, extracts cookies and CSRF token, sends a POST request to LOGOUT_PATH with _token. On HTTP 2xx/3xx, clears the local cache.
logoutWithToken(csrfToken: string): Promise<void>Same as logout(), but uses the explicitly provided token instead of the cached one. Useful if you have a refreshed token.
clearCache(): Promise<void>Deletes the cache file without contacting the server.
loadCache(): Promise<SessionData | null>Loads the cached session (no validation).
configRef: EnvConfigInternal configuration (read‑only).
EnvConfig (Configuration)Validates and normalizes configuration.
class EnvConfig {
constructor(options: EnvConfigOptions)
static fromEnv(overrides?: Partial<EnvConfigOptions>): EnvConfig
get fullLoginUrl(): string
get fullLogoutUrl(): string
hasValidCredentials(): boolean
readonly baseUrl: string
readonly loginPath: string
readonly logoutPath: string
readonly email: string
readonly password: string
}
const config = EnvConfig.fromEnv({ email: 'override@example.com' })
Reads loadEnv() and applies overrides. Throws if BASE_URL missing.
fullLoginUrl / fullLogoutUrl – fully constructed URLs (base + path, no double slashes).hasValidCredentials() – basic check: email and password non‑empty and email contains @.CacheManager (Session Persistence)Low‑level disk cache manager.
class CacheManager {
constructor(customDir?: string)
save(session: SessionData): Promise<void>
load(): Promise<SessionData | null>
clear(): Promise<void>
updateCsrfToken(newToken: string): Promise<void>
loadFresh(maxAgeMs: number): Promise<SessionData | null>
readonly cacheFilePath: string
readonly cacheDirPath: string
}
Default location: ~/.cache/libts-csrfx-auth/session.json
save(session) – writes JSON (pretty‑printed) to disk, creates directory if missing.load() – reads and parses JSON; returns null on any error (ENOENT, invalid JSON).clear() – deletes the file if it exists.updateCsrfToken(newToken) – loads existing session, updates token and timestamp, saves back (no‑op if no session).loadFresh(maxAgeMs) – loads and checks age; returns session only if Date.now() - session.timestamp < maxAgeMs.All utilities are exported from the main entry point.
parseCookies(headers: Headers): Map<string, string>
Extracts all Set-Cookie headers from a fetch response and returns a Map of name → value. Uses headers.getSetCookie() (modern API) – falls back to empty array.
buildCookieHeader(cookies: Map<string, string>): string
Serialises a cookie map into a Cookie header string: "name1=value1; name2=value2".
parseSetCookie(cookieStr: string): { name: string; value: string } | null
Parses a raw Set-Cookie header (e.g., "sessionId=abc123; HttpOnly") and returns the first name=value pair before the first semicolon. Returns null if invalid.
extractCsrfToken(html: string): string | null
Scans HTML for <input name="_token" value="..."> or <meta name="csrf-token" content="...">. Regular expressions are case‑insensitive and handle attribute order variations.
fetchWithRetry(
url: string | URL,
options?: FetchOptions,
shouldRetry?: (res: Response) => boolean
): Promise<Response>
FetchOptions extends RequestInit (except signal) and adds:
timeoutMs?: number (default 15000)maxRetries?: number (default 3)retryDelayMs?: number (default 100)retryOn?: (res: Response) => boolean (default retries only on HTTP 5xx)Throws AuthError with code NETWORK_ERROR when all attempts fail.
isSessionFresh(session: SessionData, maxAgeMs: number): boolean
Returns true if Date.now() - session.timestamp < maxAgeMs.
sessionWithCsrfToken(session: SessionData, newToken: string): SessionData
Creates a new session object with updated token and current timestamp (immutable).
All errors thrown by the library are instances of AuthError.
AuthError Classclass AuthError extends Error {
readonly code: AuthErrorCode
readonly context?: string
readonly timestamp: number
constructor(
message: string,
code: AuthErrorCode,
options?: { cause?: unknown; context?: string },
)
static fromResponse(
response: Response,
defaultCode: AuthErrorCode,
options?: { context?: string },
): AuthError
static fromUnknown(err: unknown, fallbackCode?: AuthErrorCode): AuthError
static fromStatus(status: number, body?: string): AuthError
toJSON(): Record<string, unknown>
getFormattedMessage(): string
}
fromResponse – maps HTTP status codes (see table below) to error codes.fromUnknown – intelligently extracts message/code from DOMException (AbortError), TypeError (fetch), or generic Error.toJSON() – safe for logging (includes trimmed stack).getFormattedMessage() – returns [CODE] message | Context: ... | Cause: ....AuthErrorCode)| Code | Retryable | Typical HTTP status | Description |
|---|---|---|---|
INVALID_CREDENTIALS |
– | Email or password empty / malformed. | |
CSRF_NOT_FOUND |
– | Token missing in HTML. | |
CSRF_FETCH_FAILED |
– | GET login page failed (network/4xx/5xx). | |
CSRF_EXPIRED |
419 | Token expired (server response). | |
LOGIN_FAILED |
4xx (except 419/422) | Login POST returned non‑2xx, non‑retryable. | |
LOGOUT_FAILED |
4xx (except 419/422) | Logout POST failed. | |
NOT_AUTHENTICATED |
– | No valid session in cache. | |
NETWORK_ERROR |
– | DNS, TLS, socket, or fetch throw. |
|
TIMEOUT |
– | Request aborted due to timeout. | |
CACHE_ERROR |
– | File system read/write error. | |
VALIDATION_ERROR |
400, 422 | Form validation error (e.g., wrong email/password). | |
TOO_MANY_REQUESTS |
429 | Rate limited. | |
SERVER_ERROR |
5xx | Server internal error. | |
UNAUTHORIZED |
401 | Not authenticated (missing/invalid session). | |
FORBIDDEN |
403 | Authenticated but not allowed. | |
NOT_FOUND |
404 | Endpoint does not exist. | |
UNKNOWN |
– | Catch‑all. |
Use the built‑in guards:
import { isRetryableError, isAuthenticationError } from 'libts-csrfx-auth'
try {
await auth.login()
} catch (err) {
if (isRetryableError(err)) {
// The error may resolve on a subsequent attempt (network hiccup, server overload).
// The library already retries automatically, but you can add custom logic.
}
if (isAuthenticationError(err)) {
// Credentials are wrong, CSRF expired, or user not logged in.
// Redirect to login UI, prompt for new credentials.
}
}
const auth = new AuthClient({ cacheDir: './my-session-cache' })
// Session stored in ./my-session-cache/session.json
Some servers require these headers for CSRF validation. You can disable them if not needed:
const auth = new AuthClient({ sendReferer: false, sendOrigin: false })
If you have a token from another source (e.g., API response), you can update the cache:
await auth.cacheManager.updateCsrfToken('new_token_from_api')
Or create a new session object:
const updated = sessionWithCsrfToken(oldSession, 'fresh_token')
await auth.cacheManager.save(updated)
The library uses Bun.env for environment access; in Node.js it falls back to process.env. No additional polyfills are required for fetch (Node 18+).
.
├── lib/ # Source code
│ ├── auth/ # AuthClient, LogoutClient
│ ├── handler/ # EnvConfig, loadEnv, AuthError
│ ├── utils/ # cookies, csrf, http, session
│ ├── lib.ts # Public API barrel
│ └── preloader.ts # Internal re‑exports
├── build/ # Compiled output (CJS, ESM, types)
├── docs/ # Generated HTML documentation
├── docs-md/ # Optional Markdown output
├── scripts/ # Build helpers
├── test/ # Unit tests (Bun test)
├── package.json
├── tsconfig.json
├── typedoc.json
└── rollup.config.mjs
| Command | Description |
|---|---|
bun run clean |
Delete build/ and cache. |
bun run typecheck |
Run tsc --noEmit. |
bun run format |
Format all source files with Prettier. |
bun run clean-code |
Run rmcm (remove comments) – used for distribution. |
bun run build:lib:prod |
Bundle library (minified) with Rollup. |
bun run rebuild |
Clean → typecheck → build → format → docs. |
bun run test |
Run tests with Bun. |
bun run test:coverage |
Run tests with coverage report (requires bun:test). |
bun run docs:generate |
Generate HTML docs with TypeDoc. |
bun run docs:serve |
Serve docs locally (port 8080). |
bun run version:patch |
Bump patch version in package.json. |
bun run release |
Publish to npm (runs prepublishOnly). |
Tests are written with Bun's built‑in test runner:
bun run test
bun run test:coverage
Mock HTTP responses are recommended – the library does not make real network calls during unit tests.
TypeDoc generates API documentation from the TSDoc comments.
bun run docs:generate # outputs to docs/
bun run docs:serve # serves on http://localhost:8080
package.json (or run bun run version:patch).bun run rebuild to ensure everything builds.bun run test to verify.bun run release (which runs npm publish).| Error | Likely cause | Solution |
|---|---|---|
EnvConfig: baseUrl must be a valid HTTP/HTTPS URL |
BASE_URL missing or malformed. |
Check .env or environment; ensure http:// or https://. |
CSRF token not found in HTML |
Login page HTML doesn’t contain token. | Verify LOGIN_PATH is correct; inspect the page manually. |
HTTP 419: CSRF token expired or invalid |
Token from GET page is stale. | The library retries automatically; if persistent, the server may require a fresh token per attempt. |
Validation error: email/password incorrect |
Wrong credentials (HTTP 422). | Check USER_EMAIL / USER_PASSWORD. |
No valid session found (logout) |
No cached session or loggedIn: false. |
Run auth.login() first. |
NETWORK_ERROR after retries |
Server unreachable, DNS failure, TLS error. | Check network connectivity; increase maxRetries or timeoutMs. |
console.log in your code to see the raw HTML or error snippets.timeoutMs higher for slow servers.sendReferer / sendOrigin options if the server rejects requests without those headers.cat ~/.cache/libts-csrfx-auth/session.json.AuthError.getFormattedMessage() for detailed error logs.git checkout -b feat/your-feature).feat: add new retry predicate).bun run rebuild to ensure formatting, typecheck, and tests pass.main.All contributions must pass the existing test suite and maintain 100% type coverage. New features should include tests.
AGPL-3.0-only – see LICENSE for details.
This license ensures that any network‑distributed modifications remain open source.
Repository: https://github.com/neuxdotdev/libts-csrfx-auth
Issues: https://github.com/neuxdotdev/libts-csrfx-auth/issues
npm: https://www.npmjs.com/package/libts-csrfx-auth
Documentation: https://neuxdotdev.github.io/libts-csrfx-auth/