
Security News
Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.
@wristband/typescript-session
Advanced tools
Secure, encrypted cookie-based session management for TypeScript applications.
Enterprise-ready auth that is secure by default, truly multi-tenant, and ungated for small businesses.
Website âĸ Documentation
Secure, encrypted cookie-based session management for TypeScript applications.
This SDK provides enterprise-grade session management with:
Use it standalone for any TypeScript application, or pair with Wristband for a complete multi-tenant authentication solution.
This SDK uses encrypted cookie-based sessions - a lightweight approach where session data is stored in the browser as an encrypted cookie rather than on the server.
This SDK uses encryption (AES-256-GCM) rather than just signing (like signed JWTs) to keep your session data completely private.
Perfect for modern web applications:
Consider server-side sessions (Redis, database) if you need:
Browser cookies are limited to 4KB total (per RFC 6265). After encryption overhead and cookie attributes, you have ~3KB for actual session data.
What fits in ~3KB:
// â
This fits comfortably
{
userId: "user_abc123",
tenantId: "tenant_xyz789",
accessToken: "eyJhbGc...", // JWT (~500-1000 bytes)
email: "user@example.com",
role: "admin",
preferences: { theme: "dark", language: "en" },
lastLogin: 1735689600000
}
What doesn't fit:
// â Too large
{
shoppingCart: [...100 items...], // Too much data
userHistory: [...months of activity...], // Too much data
profileData: { ...extensive user info...} // Too much data
}
Solution for large data: Store a reference ID in the session, fetch full data from database when needed:
// â
Store reference in session
session.cartId = "cart_abc123";
// â
Fetch full cart from database when needed
const cart = await db.getCart(session.cartId);
# With npm
npm install @wristband/typescript-session
# Or with yarn
yarn add @wristband/typescript-session
# Or with pnpm
pnpm add @wristband/typescript-session
Using with Wristband?
See Wristband Integration for auth-specific examples.
How it works:
getSession() to read the encrypted cookie from the request (returns either existing session data or an empty session if no cookie exists yet)save() or destroy() to persist changesFor Node.js, pass request, response, and session options to getSession(). The SDK will write cookies directly to the response object.
1: Add session middleware
Create a middleware that adds a session to every request:
// src/app.ts (Express)
import express from 'express';
import { getSession } from '@wristband/typescript-session';
const app = express();
// Add session to all requests
app.use(async (req, res, next) => {
req.session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400, // 24 hours
});
next();
});
2: Use sessions in your routes
Save session data when users log in:
app.post('/login', async (req, res) => {
// Authenticate user (your logic here)
// ...
// Store user data in session
req.session.userId = '123';
req.session.loginTime = Date.now();
await req.session.save(); // Encrypt and write cookie
res.json({ success: true });
});
Destroy session when users log out:
app.post('/logout', async (req, res) => {
req.session.destroy(); // Destroy cookie
res.json({ success: true });
});
đĄ Performance Tip
For Express applications, consider using
getSessionSync()with deferred mode to batch writes. See Deferred Mode for details.
For Edge runtimes, pass request and session options to getSession(). Instead of save() and destroy(), you'll use saveToResponse() or destroyToResponse() to clone your Response and append session cookies.
Step 1: Create a login endpoint
In Edge runtimes, you don't pass a response object to getSession(). Instead, create your Response and pass it to saveToResponse(), which clones it and adds the session cookie:
// app/api/login/route.ts (Next.js App Router)
import { getSession } from '@wristband/typescript-session';
export async function POST(request: Request) {
// Get session from request only (no response object)
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400, // 24 hours
});
// Authenticate user (your logic here)
// ...
// Store user data in session
session.userId = '123';
session.loginTime = Date.now();
// Return new Response with session cookie
const response = Response.json({ success: true });
return await session.saveToResponse(response);
}
Step 2: Create a logout endpoint
Use destroyToResponse() to destroy the session and return a Response with the deletion cookie:
// app/api/logout/route.ts (Next.js App Router)
import { getSession } from '@wristband/typescript-session';
export async function POST(request: Request) {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400, // 24 hours
});
// Destroy session and return response with deletion cookie
const response = Response.json({ success: true });
return await session.destroyToResponse(response);
}
Key Differences:
Node.js:
request, response, and config options to getSession()save() to write cookies (mutates response object)destroy() to delete cookies (mutates response object)res.setHeader()Edge Runtimes:
request and config options to getSession() (no response)saveToResponse(response) to clone and add cookiesdestroyToResponse(response) to clone and add deletion cookiesđĄ Generating Secure Secrets
Your session secret must be at least 32 characters for security. Generate one using:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Best practice: Store secrets in environment variables, never commit them to source control.
Basic setup with middleware:
import express from 'express';
import { getSession } from '@wristband/typescript-session';
const app = express();
// Session middleware
app.use(async (req, res, next) => {
req.session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
next();
});
// Login route
app.post('/login', async (req, res) => {
// Your auth logic...
const user = await authenticateUser(req.body.email, req.body.password);
req.session.userId = user.id;
req.session.email = user.email;
await req.session.save();
res.json({ success: true });
});
// Protected route
app.get('/dashboard', async (req, res) => {
if (!req.session.userId) {
return res.redirect('/login');
}
res.render('dashboard', { user: req.session });
});
// Logout route
app.post('/logout', async (req, res) => {
req.session.destroy();
res.json({ success: true });
});
Performance Optimization
For production Express apps, consider using
getSessionSync()with deferred mode to batch session writes. See Deferred Mode for details.
App Router - Route Handlers
// app/api/login/route.ts
import { getSession } from '@wristband/typescript-session';
export async function POST(request: Request) {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Your auth logic
const { email, password } = await request.json();
const user = await authenticateUser(email, password);
// Save to session
session.userId = user.id;
session.email = user.email;
const response = Response.json({ success: true });
return await session.saveToResponse(response);
}
// app/api/logout/route.ts
import { getSession } from '@wristband/typescript-session';
export async function POST(request: Request) {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
const response = Response.json({ success: true });
return session.destroyToResponse(response);
}
Middleware (Route Protection):
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getSession } from '@wristband/typescript-session';
export async function middleware(request: NextRequest) {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Protect dashboard routes
if (!session.userId && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: '/dashboard/:path*',
};
Server Actions:
Server Actions can't return Response objects, so you need to manually manage cookies via Next.js's cookies() API. You can build helper functions that use getCookieDataForSave() and getCookieDataForDestroy():
// lib/session-helpers.ts
import { cookies } from 'next/headers';
import {
getSession,
Session,
SessionOptions,
SessionData
} from '@wristband/typescript-session';
// Next.js cookie store interface (duck-typed)
interface NextJsCookieStore {
get(name: string): { value: string } | undefined;
set(name: string, value: string, options?: any): void;
}
// Get session for Server Actions
export async function getSessionForServer<T extends SessionData = SessionData>(
cookieStore: NextJsCookieStore,
options: SessionOptions
): Promise<Session<T> & T> {
const cookieName = options.cookieName || 'session'; // SDK Default
const cookieValue = cookieStore.get(cookieName)?.value;
// Create a real Web Request with the session cookie
const request = new Request('https://placeholder.local', {
headers: { cookie: cookieValue ? `${cookieName}=${cookieValue}` : '' },
});
return await getSession<T>(request, options);
}
// Save session for Server Actions
export async function saveSessionForServer<T extends SessionData = SessionData>(
cookieStore: NextJsCookieStore,
session: Session<T> & T
): Promise<void> {
const cookieData = await session.getCookieDataForSave();
for (const { name, value, options } of cookieData) {
cookieStore.set(name, value, options);
}
}
// Destroy session for Server Actions
export function destroySessionForServer<T extends SessionData = SessionData>(
cookieStore: NextJsCookieStore,
session: Session<T> & T
): void {
const cookieData = session.getCookieDataForDestroy();
for (const { name, value, options } of cookieData) {
cookieStore.set(name, value, options);
}
}
Then use the helpers in your Server Actions:
// app/actions.ts
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import {
destroySessionForServer,
getSessionForServer,
saveSessionForServer
} from '@/lib/session';
const sessionOptions = {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
};
export async function loginAction(formData: FormData) {
const cookieStore = await cookies();
const session = await getSessionForServer(cookieStore, sessionOptions);
// Your auth logic
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const user = await authenticateUser(email, password);
// Save user to session
session.userId = user.id;
session.email = user.email;
await saveSessionForServer(cookieStore, session);
redirect('/dashboard');
}
export async function updatePreferences(theme: string) {
const cookieStore = await cookies();
const session = await getSessionForServer(cookieStore, sessionOptions);
if (!session.userId) {
redirect('/login');
}
// Update session
session.preferences = { theme };
await saveSessionForServer(cookieStore, session);
}
export async function logoutAction() {
const cookieStore = await cookies();
const session = await getSessionForServer(cookieStore, sessionOptions);
destroySessionForServer(cookieStore, session);
redirect('/login');
}
export default {
async fetch(request: Request): Promise<Response> {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 24 * 60 * 60, // 24 hours
});
const url = new URL(request.url);
if (url.pathname === '/login' && request.method === 'POST') {
// Your auth logic
const { email, password } = await request.json();
const user = await authenticateUser(email, password);
// Save to session
session.userId = user.id;
session.email = user.email;
const response = Response.json({ success: true });
return await session.saveToResponse(response);
}
if (url.pathname === '/dashboard') {
if (!session.userId) {
return Response.redirect(new URL('/login', request.url));
}
return Response.json({
message: 'Welcome to dashboard',
user: { id: session.userId, email: session.email },
});
}
if (url.pathname === '/logout' && request.method === 'POST') {
const response = Response.json({ success: true });
return session.destroyToResponse(response);
}
return Response.json({ error: 'Not found' }, { status: 404 });
},
};
đĄ Learn More
For more documentation on
getCookieDataForSave()andgetCookieDataForDestroy(), see the Next.js Server Actions Methods section in the API Reference.
Retrieves an existing session or creates a new empty session. This is the primary function for session management.
Signature:
// Node.js (with response object)
function getSession<T extends SessionData = SessionData>(
request: IncomingMessage | Request,
response: ServerResponse | Response,
options: SessionOptions
): Promise<Session<T> & T>
// Edge Runtimes (without response object)
function getSession<T extends SessionData = SessionData>(
request: Request,
options: SessionOptions
): Promise<Session<T> & T>
Parameters:
request - The incoming HTTP request (Node.js IncomingMessage, NextApiRequest, Express Request, or Web Request)response - The HTTP response object (Node.js only; not needed in Edge runtimes)options - Session configuration (see SessionOptions)Returns:
Promise<Session<T> & T> - A promise resolving to a session instance that provides both session management methods and direct access to the underlying data.
Throws:
SessionError if configuration is invalid or request type is unsupported
Example:
// Node.js / Express
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Edge Runtime (Next.js, Cloudflare Workers, etc.)
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
Synchronous version of getSession() for Node.js environments. Used primarily with deferred mode in Express for better performance.
Signature:
function getSessionSync<T extends SessionData = SessionData>(
request: IncomingMessage | Request,
response: ServerResponse | Response,
options: SessionOptions
): Session<T> & T
Parameters:
Same as getSession()
Returns:
Session<T> & T - A session instance that provides both session management methods and direct access to the underlying data.
Example:
import { getSessionSync } from '@wristband/typescript-session';
app.use((req, res, next) => {
req.session = getSessionSync(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
req.session.enableDeferredMode();
const prevWriteHead = res.writeHead.bind(res);
res.writeHead = function(...args) {
if (!res.headersSent) {
req.session.flushSync();
}
return prevWriteHead(...args);
};
next();
});
See Deferred Mode for more details.
These SDK configuration options control how sessions are created, encrypted, and stored in cookies. All cookie-related options follow standard HTTP cookie specifications.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
secrets | string or string[] | Yes | N/A | Secret key(s) used for encrypting and decrypting session cookie data. Single string: Used for both encryption and decryption. Array of strings: First key encrypts new sessions, all keys tried for decryption (enables key rotation). Example: [process.env.NEW_SECRET, process.env.OLD_SECRET] Secrets must be at least 32 characters for security. Use cryptographically random strings (e.g., from a secure password generator). |
cookieName | string | No | 'session' | The name of the session cookie to set in the response. |
maxAge | number | No | 3600 (1 hour) | Session expiration time in seconds. Determines how long a session remains valid. After this time, the session cookie expires and users must re-authenticate. Example: 86400 for 24 hours, 604800 for 7 days. |
path | string | No | '/' | Cookie path attribute. Specifies which URL paths can access the cookie. Default '/' means all paths. |
secure | boolean | No | true | Cookie Secure flag. If true, cookie is only sent over HTTPS connections. Security recommendation: Always use true in production. Only set to false for local development over HTTP. |
sameSite | 'Strict', 'Lax', or 'None' | No | 'Lax' | Cookie SameSite setting. Controls whether cookies are sent on cross-site requests. 'Strict': Cookies only sent for same-site requests (recommended, when possible). 'Lax': Cookies sent for same-site requests plus top-level navigation GET requests (good balance). 'None': Cookies sent for all requests (requires secure: true; use with CSRF token protection enabled; use only if you know what you are doing). |
domain | string | No | undefined | Cookie domain attribute. Controls which domains can access the cookie. undefined: Cookie only sent to current domain. '.example.com': Cookie sent to example.com and all subdomains. 'app.example.com': Cookie only sent to app.example.com. |
enableCsrfProtection | boolean | No | false | Enable CSRF protection by generating and validating CSRF tokens. Recommendation: Only enable if you use sameSite: 'None' or want defense-in-depth. The default sameSite: 'Lax' (or 'Strict') provides sufficient CSRF protection for most cases. When enabled: CSRF token is automatically generated after authentication (via any save() function), stored in both session and a separate cookie, and must be included in request headers for server validation. When disabled: No CSRF tokens or cookies are generated. |
csrfCookieName | string | No | 'CSRF-TOKEN' | Name of the CSRF cookie. Only used if enableCsrfProtection is true. |
csrfCookieDomain | string | No | undefined | Domain for CSRF cookie. Follows same rules as domain option. Falls back to domain value if not specified. Only used if enableCsrfProtection is true. |
The session object supports direct property access for reading and writing data. Direct access is fully type-safe when using TypeScript with a custom session data type.
// Reading data
const userId = session.userId; // Same as session.get('userId')
const theme = session.theme; // Same as session.get('theme')
// Writing data
session.userId = '123'; // Same as session.set('userId', '123')
session.theme = 'dark'; // Same as session.set('theme', 'dark')
// Deleting data
delete session.theme; // Same as session.delete('theme')
You can also use any of the methods below to update your session data.
Once you have a session object, you can use these methods:
get(key)Type-safe getter for session data.
const userId = session.get('userId'); // string | undefined
const theme = session.get('theme'); // 'light' | 'dark' | undefined
Parameters:
key: keyof T - The session data keyReturns:
T[key] | undefined
set(key, value)Type-safe setter for session data.
session.set('userId', '123');
session.set('theme', 'dark');
session.set('lastVisit', Date.now());
Parameters:
key: keyof T - The session data keyvalue: T[key] - The value to setReturns:
void
Throws:
SessionError if session has been destroyed
delete(key)Delete a key from session data.
session.delete('theme');
session.delete('lastVisit');
Parameters:
key: keyof T - The session data key to deleteReturns:
void
Throws:
SessionError if session has been destroyed
toJSON()Returns the session data as a plain object. This method is automatically called by JSON.stringify() and enables clean serialization of the session.
const sessionData = session.toJSON();
console.log(sessionData); // { userId: '123', theme: 'dark', ... }
// Also called automatically by JSON.stringify()
const json = JSON.stringify(session);
console.log(json); // '{"userId":"123","theme":"dark",...}'
Parameters:
None
Returns:
T - A plain object containing all session data fields
These methods work in Node.js environments (Express, Next.js Pages Router, etc.):
save()Encrypts session data and writes the cookie (mutates the response object). If enableCsrfProtection is true in the SDK configuration, a CSRF token is generated, stored in session.csrfToken, and written to the CSRF cookie.
đĄ For Edge runtimes, use
saveToResponse().
await session.save();
Returns:
Promise<void>
Throws:
SessionError if:
destroy()Deletes the underlying session data as well as the session cookie from the response (mutates the response object). If enableCsrfProtection is true in the SDK configuration, then the CSRF cookie is deleted as well.
đĄ For Edge runtimes, use
destroyToResponse().
session.destroy();
Returns:
void
enableDeferredMode()Enables deferred mode to batch cookie writes. Call this before any save() operations, then use flush() or flushSync() to write cookies at the end of the request.
See Deferred Mode for more details.
session.enableDeferredMode();
session.set('userId', '123');
await session.save(); // Marks for save (doesn't write yet)
await session.flush(); // Actually writes cookies
Returns:
void
flush()Asynchronously flush pending session changes. Used with deferred mode.
await session.flush();
Returns:
Promise<void>
Throws:
SessionError if:
flushSync()Synchronously flush pending session changes using Node.js crypto. Used with deferred mode.
session.flushSync();
Returns:
void
Throws:
SessionError if:
These methods work in Edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy, etc.):
saveToResponse(response)Encrypts session data and returns a new Response with the session cookie appended. If enableCsrfProtection is true in the SDK configuration, a CSRF token is generated, stored in session.csrfToken, and written to the CSRF cookie.
đĄ For Node.js runtimes, use
save().
session.userId = '123';
const response = new Response('Success');
return await session.saveToResponse(response);
Parameters:
response: Response - The response to clone and append cookies toReturns:
Promise<Response> - A Promise that resolves to a new Response object with session cookies
destroyToResponse(response)Destroys the session and returns a new Response with deletion cookies appended. If enableCsrfProtection is true in the SDK configuration, then the CSRF cookie is deleted as well.
đĄ For Node.js runtimes, use
destroy().
const response = new Response('Logged out');
return session.destroyToResponse(response);
Parameters:
response: Response - The response to clone and append deletion cookies toReturns:
Response - A new Response object with deletion cookies
These are low-level methods for framework adapters that require manual cookie management. Next.js Server Actions can't return Response objects, so you can't use saveToResponse() or destroyToResponse(). Instead, these methods return cookie data that can be set via Next.js's cookies() API.
đĄ Looking for examples? See the Next.js Platform Examples section for complete helper patterns and usage examples with Server Actions.
getCookieDataForSave()Returns prepared cookie data for manual cookie setting (e.g., Next.js cookies().set()). If enableCsrfProtection is true, it will automatically generate a CSRF token and include the CSRF cookie data.
'use server';
import { cookies } from 'next/headers';
export async function loginAction() {
const session = await getSession(/* ... */);
session.set('userId', '123');
const cookieData = await session.getCookieDataForSave();
const cookieStore = await cookies();
for (const { name, value, options } of cookieData) {
cookieStore.set(name, value, options);
}
}
Returns:
Promise<Array<{ name: string; value: string; options: CookieOptions }>> -
A Promise that resolves to an array of cookie objects, where each object contains:
name (string): The cookie name (e.g., 'session' or 'CSRF-TOKEN')
value (string): The encrypted session data (for session cookie) or CSRF token value (for CSRF cookie)
options (CookieOptions): Cookie configuration object with the following properties:
The returned array will contain:
enableCsrfProtection is true)Throws:
SessionError if:
getCookieDataForDestroy()Returns prepared cookie deletion data for manual cookie deletion. Returns cookies with empty values and expiration dates in the past. If enableCsrfProtection is true, includes CSRF cookie deletion data.
const cookieData = session.getCookieDataForDestroy();
const cookieStore = await cookies();
for (const { name, value, options } of cookieData) {
cookieStore.set(name, value, options);
}
Returns:
Array<{ name: string; value: string; options: CookieOptions }> - An array of cookie deletion objects with the same structure as getCookieDataForSave(), but with:
The returned array will contain:
enableCsrfProtection is true)These methods are only relevant when using Wristband authentication for the Backend Server Integration Pattern:
fromCallback(callbackData, customFields?)Populates session from Wristband authentication callback data.
const callbackResult = await wristbandAuth.callback(req);
session.fromCallback(callbackResult.callbackData, {
preferences: { theme: 'dark' },
lastLogin: Date.now(),
});
await session.save();
Parameters:
callbackData: CallbackData - Data from Wristband auth callbackcustomFields?: Record<string, any> - Optional custom fields to merge (must be JSON-serializable)Returns:
void
Throws:
SessionError if:
callbackData is missing or invalidcustomFields are not JSON-serializablegetSessionResponse(metadata?)Returns formatted session response for Wristband frontend SDKs.
app.get('/api/v1/session', async (req, res) => {
const session = await getSession(req, res, { secrets: process.env.SESSION_SECRET });
const response = session.getSessionResponse({
name: session.fullName,
preferences: session.preferences,
});
res.json(response);
});
Parameters:
metadata?: Record<string, any> - Optional metadata to includeReturns:
{ tenantId: string; userId: string; metadata?: Record<string, any> }
Throws:
SessionError if session is not authenticated
getTokenResponse()Returns formatted token response for Wristband frontend SDKs.
app.get('/api/v1/token', async (req, res) => {
const session = await getSession(req, res, { secrets: process.env.SESSION_SECRET });
const response = session.getTokenResponse();
res.json(response);
});
Returns:
{ accessToken: string; expiresAt: number }
Throws:
SessionError if session is not authenticated or missing token data
The SDK uses a structured error system with specific error codes for different failure scenarios. All errors thrown by the SDK are instances of SessionError with a code property for programmatic error handling.
import { SessionError, SessionErrorCode } from '@wristband/typescript-session';
try {
await session.save();
} catch (error) {
if (error instanceof SessionError) {
console.error('Error code:', error.code);
console.error('Message:', error.message);
// Optional: inspect underlying cause for debugging
if (error.cause) {
console.error('Caused by:', error.cause);
}
}
}
Session Lifecycle Errors
| Code | Description | Common Causes |
|---|---|---|
SESSION_DESTROYED | Attempted to modify or save a destroyed session | Calling set(), save(), or other mutation methods after destroy() |
SESSION_SAVE_FAILED | Failed to save session | Encryption failure, cookie size exceeded, serialization error |
Configuration Errors
| Code | Description | Common Causes |
|---|---|---|
INVALID_CONFIGURATION | Invalid session configuration | Missing/invalid secrets, unsupported request type, attempting to override session methods |
MISSING_RESPONSE | No response object available for cookie operations | Calling save() in Edge runtime (use saveToResponse() instead), missing response parameter |
Deferred Mode Errors
| Code | Description | Common Causes |
|---|---|---|
DEFERRED_MODE_NOT_ENABLED | Attempted to flush without enabling deferred mode | Calling flush() or flushSync() without calling enableDeferredMode() first |
Wristband-Specific Errors
| Code | Description | Common Causes |
|---|---|---|
CALLBACK_DATA_INVALID | Invalid or missing callback data | Missing callbackData or callbackData.userinfo in fromCallback() |
CUSTOM_FIELDS_NOT_SERIALIZABLE | Custom fields cannot be serialized to JSON | Passing non-serializable values (functions, circular references) to fromCallback() |
SESSION_NOT_AUTHENTICATED | Operation requires authenticated session | Calling getSessionResponse() or getTokenResponse() on unauthenticated session |
try {
const session = await getSession(req, res, {
secrets: 'short' // â Too short (must be 32+ chars)
});
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.INVALID_CONFIGURATION) {
console.error('Configuration error:', error.message);
// "Secrets must be at least 32 characters long for security"
}
}
// â Wrong - save() requires response object
const session = await getSession(request, { secrets: env.SESSION_SECRET });
session.set('userId', '123');
await session.save(); // Throws MISSING_RESPONSE
// â
Correct - use saveToResponse() in Edge runtimes
const session = await getSession(request, { secrets: env.SESSION_SECRET });
session.set('userId', '123');
const response = new Response('Success');
return session.saveToResponse(response);
const session = await getSession(req, res, { secrets: process.env.SESSION_SECRET });
session.destroy();
try {
session.set('userId', '123'); // â Throws SESSION_DESTROYED
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.SESSION_DESTROYED) {
console.error('Cannot modify destroyed session');
}
}
try {
session.fromCallback(null); // â Missing callback data
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.CALLBACK_DATA_INVALID) {
console.error('Invalid callback data:', error.message);
}
}
const session = await getSession(req, res, { secrets: process.env.SESSION_SECRET });
session.set('userId', '123');
try {
await session.flush(); // â Throws DEFERRED_MODE_NOT_ENABLED
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.DEFERRED_MODE_NOT_ENABLED) {
console.error('Must enable deferred mode first');
}
}
// â
Correct usage
session.enableDeferredMode();
await session.save(); // Just marks for save
await session.flush(); // Actually saves
try {
await session.save();
} catch (error: unknown) {
if (error instanceof SessionError) {
console.error('Session save failed:', {
code: error.code,
message: error.message,
stack: error.stack,
cause: error.cause ?? 'No underlying cause'
});
} else {
// Unexpected error type
console.error('Unexpected error during session save:', error);
}
}
// Validate session config at startup
try {
const testSession = await getSession(mockRequest, {
secrets: process.env.SESSION_SECRET,
maxAge: 3600
});
console.log('Session configuration valid â');
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.INVALID_CONFIGURATION) {
console.error('Invalid session configuration:', error.message);
process.exit(1);
}
}
âšī¸ Note on Deferred Mode
For most applications, the default behavior (encrypt on
save()) is perfectly fine. Only use deferred mode if you're modifying sessions multiple times per request or profiling shows encryption is a bottleneck. Start simple, optimize only if needed.
The problem:
By default, calling save() encrypts session data immediately. If you modify the session multiple times per request, each change triggers encryption:
session.userId = '123';
await session.save(); // Encrypts
session.lastActivity = Date.now();
await session.save(); // Encrypts again
This can become inefficient at scale for applications that modify sessions frequently.
The solution: Deferred Mode
Deferred Mode batches all saved session changes and encrypts once when you explicitly flush:
session.enableDeferredMode(); // Saves now only track if data has changed
session.userId = '123';
await session.save(); // Session knows to encrypt all changes upon flush
session.pageViews = 5;
await session.save(); // Behaves the same as the first save() call
await session.flush(); // Finally encrypts just once with all changes
Express implementation:
Express middleware runs synchronously, so you can use getSessionSync() and flushSync() to avoid async operations in the middleware chain. Hook into the response lifecycle to flush automatically before headers are sent:
import { getSessionSync } from '@wristband/typescript-session';
app.use((req, res, next) => {
req.session = getSessionSync(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
req.session.enableDeferredMode();
// Auto-flush before headers are sent
const prevWriteHead = res.writeHead.bind(res);
res.writeHead = function(...args) {
if (!res.headersSent) {
req.session.flushSync(); // Synchronous flush
}
return prevWriteHead(...args);
};
next();
});
Now your routes can modify the session without manual saves:
app.get('/dashboard', (req, res) => {
req.session.lastActivity = Date.now();
req.session.pageViews = (req.session.pageViews || 0) + 1;
req.session.save() // Queued
res.json({ views: req.session.pageViews });
// Encryption happens automatically before response is sent
});
This SDK includes optional CSRF token protection for defense-in-depth security against cross-site request forgery attacks.
How it works:
When enabled, this SDK implements the Synchronizer Token Pattern, a widely-used CSRF defense recommended by OWASP. It works by the following:
save(), saveToResponse(), or getCookieDataForSave()csrfToken field)CSRF-TOKEN)X-CSRF-TOKEN)destroy(), destroyToResponse(), or getCookieDataForDestroy(), both the session cookie and CSRF cookie are automatically deletedThis ensures that even if an attacker can trick a user's browser into making a request, they cannot read the token from the cookie (due to browser same-origin policy) and therefore cannot include it in the attack request.
For more details on CSRF prevention best practices, see the OWASP CSRF Prevention Cheat Sheet.
â ī¸ Implementation Required
This SDK handles token generation, storage, and cookie management. However, you must implement:
- Frontend: Reading the CSRF cookie and including it in request headers
- Backend: Validating the header token matches the session token
See the "Example usage" section below for implementation patterns.
CSRF token protection is disabled by default (enableCsrfProtection: false). The SDK relies on sameSite: 'Lax' (or 'Strict') cookies for CSRF protection, which is sufficient for most applications.
When to enable:
You should enable CSRF tokens if:
sameSite: 'None' (e.g., embedded iframes)Basic configuration:
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
enableCsrfProtection: true, // Enables token-based protection
});
Custom configuration:
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
enableCsrfProtection: true,
csrfCookieName: 'MY_APP_CSRF', // Default: 'CSRF-TOKEN'
csrfCookieDomain: '.example.com', // Default: same as session cookie domain
});
Example usage:
The CSRF token is auto-generated when saving the session in the backend:
session.isAuthenticated = true;
await session.save(); // Generates and saves csrfToken
The frontend reads that token from CSRF cookie:
// Get token from the cookie
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('CSRF-TOKEN='))
?.split('=')[1];
// Send token in request header
fetch('/api/protected', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({ data: 'bodyData' }),
});
The backend validates the token value in the request header matches the csrfToken value in the session data:
app.post('/api/protected', async (req, res) => {
const session = await getSession(req, res, sessionOptions);
if (!session.isAuthenticated) {
return res.status(401).json({ error: 'Not authenticated' });
}
// Validate CSRF token
const headerToken = req.headers['X-CSRF-TOKEN'];
if (session.csrfToken !== headerToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Process request...
});
CSRF cleanup on logout:
When you destroy a session using any of the destroy() functions, both the session cookie and CSRF cookie are deleted:
app.post('/logout', async (req, res) => {
const session = await getSession(req, res, sessionOptions);
session.destroy(); // Clears session data AND deletes CSRF cookie
res.json({ success: true });
});
Both cookies are removed from the browser, ensuring complete cleanup of session state.
The SDK supports seamless secret rotation without invalidating existing sessions.
How it works:
const session = await getSession(req, res, {
secrets: [
'new-secret-key-min-32-characters-long', // Used for NEW sessions
'old-secret-key-min-32-characters-long', // Used to DECRYPT old sessions
],
maxAge: 86400000,
});
Encryption: Always uses the first secret (secrets[0])
Decryption: Tries each secret in order until one works:
new-secret-key â Success? Doneold-secret-key â Success? DoneRotation strategy:
['new', 'old']maxAge duration (all old sessions expire naturally)['new']This allows zero-downtime secret rotation. Use up to 3 secrets to support gradual rollout across multiple deployments.
Sessions automatically roll on every save, meaning the session expiration time resets to the current time plus maxAge. This keeps active users logged in without requiring re-authentication.
How it works:
// 1) Session created at 1:00 PM, expires at 2:00 PM
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 3600, // 1 hour
});
// 2) User makes a request at 1:30 PM...
session.lastActivity = Date.now();
await session.save();
//
// 3) Session now expires at 2:30 PM (1:30 PM + 1 hour)
//
All save operations roll the session:
// All save-related functions reset expiration from current time
session.save()
session.saveToResponse()
session.getCookieDataForSave()
The SDK is built with full TypeScript support and type safety. The SessionData interface includes optional Wristband fields, but you can extend it with any custom fields you want:
interface SessionData {
// Wristband fields (optional; can be used without Wristband)
isAuthenticated?: boolean;
userId?: string;
tenantId?: string;
tenantName?: string;
tenantCustomDomain?: string;
identityProviderName?: string;
accessToken?: string;
expiresAt?: number;
refreshToken?: string;
csrfToken?: string;
// Your custom fields
[key: string]: any;
}
To add custom fields to your session for full Typescript support, do the following:
1: Create a type that extends SessionData
// src/types.ts
import { SessionData } from '@wristband/typescript-session';
interface MySessionData extends SessionData {
cartId?: string;
theme?: 'light' | 'dark';
lastVisit?: number;
}
2: Initialize sessions with your type
import { getSession } from '@wristband/typescript-session';
import { MySessionData } from './types';
const session = await getSession<MySessionData>(req, options);
3: Use your session with full type safety
// TypeScript knows these fields exist and their types
session.cartId = '123'; // â
Valid
session.theme = 'dark'; // â
Valid
session.theme = 'potato'; // â Type error
session.lastVisit = Date.now(); // â
Valid
await session.save();
Express: Typing
req.sessionIf you're attaching the session to Express's
Requestobject via middleware, you'll need to augment the module to typereq.session:// src/types.ts import '@wristband/typescript-session'; declare module '@wristband/typescript-session' { interface SessionData { cartId?: string; theme?: 'light' | 'dark'; lastVisit?: number; } }This makes
req.sessionfully typed throughout your Express app:app.get('/cart', (req, res) => { const cartId = req.session.cartId; // â TypeScript knows this field exists // ... });
Best Practices
secure: true in production to ensure cookies are only sent over HTTPSsameSite: 'Lax' (default) or 'Strict' to prevent cross-site requests. Enable token-based CSRF protection via enableCsrfProtection: true for additional security.maxAge based on your application's security requirements (e.g., 1 hour)Threat Model
| Attack Vector | Mitigation |
|---|---|
| Cookie theft (XSS) | â HttpOnly prevents JavaScript access |
| Man-in-the-middle | â Secure flag requires HTTPS |
| CSRF attacks | â SameSite cookies + optional CSRF token protection |
| Session replay | â Timestamp-based expiration |
| Cookie tampering | â Authenticated encryption detects modifications |
| Brute force | â 256-bit encryption provides strong protection |
When using with Wristband authentication, this SDK provides helper methods to simplify common auth workflows.
After successful Wristband authentication, use fromCallback() in your Callback Endpoint to populate session data from the callback result:
import { wristbandAuth } from './wristband-config';
import { getSession } from '@wristband/typescript-session';
app.get('/auth/callback', async (req, res) => {
// Complete Wristband auth flow
const callbackResult = await wristbandAuth.callback(req);
// Get session
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Populate session from Wristband callback data
session.fromCallback(callbackResult.callbackData, {
// Optional: Add custom fields
preferences: { theme: 'dark' },
lastLogin: Date.now(),
});
await session.save();
res.redirect(callbackResult.callbackData.returnUrl || '/dashboard');
});
What fromCallback() does:
Extracts authentication data from the callback and stores it in the session:
isAuthenticated: trueaccessToken: JWT access tokenexpiresAt: Token expiration timestampuserId: User IDtenantId: Tenant IDtenantName: Tenant nameidentityProviderName: Identity providerrefreshToken: Refresh token (if offline_access scope used)tenantCustomDomain: Tenant custom domain (if applicable)Any custom fields you pass are merged with the core session data.
The getSessionResponse() method returns session data in the format expected by Wristband frontend SDKs for your Session Endpoint:
app.get('/api/v1/session', async (req, res) => {
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Return formatted session response with optional custom metadata
const response = session.getSessionResponse({
name: session.fullName,
email: session.email,
});
res.json(response);
});
Response format:
{
"tenantId": "tenant_abc123",
"userId": "user_xyz789",
"metadata": {
"name": "John Doe",
"preferences": { "theme": "dark" }
}
}
The getTokenResponse() method returns the access token and expiration time in the format expected by Wristband frontend SDKs for your Token Endpoint:
app.get('/api/v1/token', async (req, res) => {
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Return formatted token response
const response = session.getTokenResponse();
res.json(response);
});
Response format:
{
"accessToken": "eyJhbGc...",
"expiresAt": 1735689600000
}
Your frontend can use this token for direct API calls to Wristband or other services:
const tokenResponse = await fetch('/api/v1/token');
const { accessToken } = await tokenResponse.json();
// Use token to call Wristband API
const userResponse = await fetch('https://your-app.wristband.dev/api/v1/users/me', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
This session SDK is embedded in the following Wristband authentication SDKs, providing both authentication and session management in a single dependency:
If you're using Wristband for authentication, we recommend using one of these framework-specific SDKs instead of installing the session SDK separately. They provide a complete, integrated solution for both authentication and session management.
Note: You only need to install this standalone session SDK if:
Reach out to the Wristband team at support@wristband.dev for any questions regarding this SDK.
FAQs
Secure, encrypted cookie-based session management for TypeScript applications.
We found that @wristband/typescript-session demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.