@periodic/tungsten-session
v1.0.0
Published
Production-grade session orchestration for Node.js with sliding expiry, idle timeout, and storage-agnostic adapters
Maintainers
Readme
🔑 Periodic Tungsten Session
Production-grade session orchestration for Node.js with sliding expiry, idle timeout, and storage-agnostic adapters
Part of the Periodic series of Node.js packages by Uday Thakur.
💡 Why Tungsten Session?
@periodic/tungsten-session is the session management layer of the @periodic/tungsten authentication family. While @periodic/tungsten handles cryptographic primitives — signing tokens, hashing passwords, verifying HMAC signatures — this library handles the server-side session lifecycle: creating sessions, validating them on every request, rotating IDs after privilege escalation, enforcing idle timeouts, and revoking sessions on logout or security events.
Sessions are deceptively simple until they aren't. A session that never expires is a permanent backdoor. A session that doesn't rotate on login is vulnerable to fixation attacks. A session store without support for bulk revocation leaves you powerless when a user's account is compromised. Tungsten Session handles all of it with a clean, storage-agnostic interface that works identically across Redis, PostgreSQL, DynamoDB, and any backend you choose.
The name represents:
- Durability: Sessions persist correctly across requests, processes, and server restarts
- Precision: Absolute expiry, sliding expiry, and idle timeout are independently configurable
- Security: Rotation, revocation, and timing-safe lookups are built in — not bolted on
- Flexibility: The storage interface is minimal — adapt it to any backend in minutes
Just as @periodic/tungsten handles the cryptographic layer without shortcuts, @periodic/tungsten-session handles the session layer without assumptions about your infrastructure.
🎯 Why Choose Tungsten Session?
Session management seems simple until you look closely — and most implementations miss at least one critical detail:
- No session rotation leaves users vulnerable to session fixation attacks on login
- No idle timeout means an inactive session stays valid until absolute expiry — a long window for hijacking
- Absolute expiry only logs users out mid-task on long operations instead of after true inactivity
- No bulk revocation means there's no way to log a user out of all devices after a password change
- No storage abstraction means the session logic is tightly coupled to Redis or a specific ORM
- Timing-unsafe lookups leak information about which session IDs exist via response time differences
Periodic Tungsten Session provides the perfect solution:
✅ Storage-agnostic — Redis, PostgreSQL, DynamoDB, or any custom adapter
✅ Session creation — cryptographically secure IDs, stored with full metadata
✅ Session validation — checks absolute and idle expiry in a single call
✅ Sliding expiration — idle timeout resets on each validated access
✅ Absolute expiration — hard maximum lifetime, never extended
✅ Idle timeout — separate inactivity window independent of absolute expiry
✅ Session rotation — new ID on every sensitive operation, prevents fixation
✅ Single revocation — invalidate one session by ID on explicit logout
✅ Bulk revocation — invalidate all sessions for a user on security events
✅ Timing-safe operations — no information leakage via response timing
✅ Type-safe — strict TypeScript throughout, zero any
✅ No global state — no side effects on import
✅ Production-ready — non-blocking, never crashes your app
📦 Installation
npm install @periodic/tungsten-session @periodic/tungstenOr with yarn:
yarn add @periodic/tungsten-session @periodic/tungsten🚀 Quick Start
import { createSession, validateSession } from '@periodic/tungsten-session';
// 1. Define a storage adapter for your backend
const storage: SessionStorage = {
async get(id: string) {
const raw = await redis.get(`session:${id}`);
return raw ? JSON.parse(raw) : null;
},
async set(session: Session) {
await redis.set(`session:${session.id}`, JSON.stringify(session), 'EX', 604800);
},
async delete(id: string) {
await redis.del(`session:${id}`);
},
async deleteByUserId(userId: string) {
const keys = await redis.keys(`session:user:${userId}:*`);
if (keys.length) await redis.del(...keys);
return keys.length;
},
};
// 2. Create a session on login
const { sessionId, session } = await createSession('user_123', {
storage,
ttl: '7d',
idleTimeout: '24h',
});
// Set as a cookie
res.cookie('sid', sessionId, { httpOnly: true, secure: true, sameSite: 'strict' });
// 3. Validate on each request
const result = await validateSession(req.cookies.sid, {
storage,
extendOnAccess: true,
});
if (result.valid) {
req.userId = result.session.userId;
}Example session object:
{
"id": "sess_01HQ4K2N8XVPQ3Z...",
"userId": "user_123",
"createdAt": 1708000000000,
"lastAccessedAt": 1708003600000,
"expiresAt": 1708604800000,
"idleExpiresAt": 1708090000000
}🧠 Core Concepts
The Session Lifecycle
Five functions cover the complete lifecycle — each does exactly one thing:
createSession— generates a secure session ID, builds the session record, stores it, returns the IDvalidateSession— looks up the session, checks absolute and idle expiry, optionally extends idle TTLrotateSession— atomically replaces the session ID — use after privilege escalation (login, sudo)revokeSession— deletes a session by ID — use on explicit logoutrevokeAllUserSessions— deletes all sessions for a user — use on password change or security event
Design principle:
The session functions are pure orchestration — they delegate all storage to your adapter. The same call works identically with Redis, Postgres, or an in-memory store.
Expiry Model
Three independent expiry mechanisms work together:
Session created
│
├── Absolute expiry (ttl) — hard maximum, never extended
│
└── Idle timeout (idleTimeout) — reset on each validated access
Session valid while: now < expiresAt AND now < idleExpiresAtA session expires when either limit is crossed — whichever comes first. The idle timeout resets on every call to validateSession with extendOnAccess: true. The absolute expiry never changes.
The SessionStorage Interface
The only contract between tungsten-session and your infrastructure — four methods, nothing more:
interface SessionStorage {
get(id: string): Promise<Session | null>;
set(session: Session): Promise<void>;
delete(id: string): Promise<void>;
deleteByUserId(userId: string): Promise<number>; // returns count deleted
}✨ Features
🆕 Session Creation
const { sessionId, session } = await createSession('user_123', {
storage,
ttl: '7d', // absolute expiry — '7d', '24h', '30m', or milliseconds
idleTimeout: '24h', // idle expiry — reset on each access
metadata: { // optional — attach any extra data to the session
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
},
});✅ Session Validation
Validate and optionally extend the idle timeout in a single call:
const result = await validateSession(sessionId, {
storage,
extendOnAccess: true,
});
if (result.valid) {
console.log(result.session.userId);
} else {
// result.reason: 'not_found' | 'expired' | 'idle_expired'
console.log('Invalid:', result.reason);
}🔄 Session Rotation
Replace the session ID atomically after sensitive operations — prevents fixation:
const { newSessionId } = await rotateSession(currentSessionId, { storage });
res.cookie('sid', newSessionId, { httpOnly: true, secure: true, sameSite: 'strict' });🚪 Session Revocation
// Single session — explicit logout
await revokeSession(req.cookies.sid, storage);
res.clearCookie('sid');
// All sessions — password change, account compromise
const count = await revokeAllUserSessions('user_123', storage);
console.log(`Revoked ${count} sessions`);📚 Common Patterns
1. Express Session Middleware
import { validateSession } from '@periodic/tungsten-session';
export async function sessionMiddleware(req, res, next) {
const sessionId = req.cookies?.sid;
if (!sessionId) return next();
const result = await validateSession(sessionId, { storage, extendOnAccess: true });
if (result.valid) {
req.session = result.session;
req.userId = result.session.userId;
} else {
res.clearCookie('sid');
}
next();
}2. Login with Session Creation
import { verifyPassword } from '@periodic/tungsten';
import { createSession } from '@periodic/tungsten-session';
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findOne({ email });
const isValid = await verifyPassword(password, user.passwordHash);
if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });
const { sessionId } = await createSession(user.id, {
storage,
ttl: '7d',
idleTimeout: '24h',
metadata: { ipAddress: req.ip, userAgent: req.headers['user-agent'] },
});
res.cookie('sid', sessionId, { httpOnly: true, secure: true, sameSite: 'strict' });
res.json({ userId: user.id });
});3. Logout
import { revokeSession } from '@periodic/tungsten-session';
app.post('/auth/logout', async (req, res) => {
if (req.cookies.sid) {
await revokeSession(req.cookies.sid, storage);
res.clearCookie('sid');
}
res.sendStatus(204);
});4. Logout All Devices
import { revokeAllUserSessions } from '@periodic/tungsten-session';
app.post('/auth/logout-all', requireAuth, async (req, res) => {
const count = await revokeAllUserSessions(req.userId, storage);
res.clearCookie('sid');
res.json({ revokedSessions: count });
});5. Password Change with Full Revocation
import { hashPassword } from '@periodic/tungsten';
import { revokeAllUserSessions, createSession } from '@periodic/tungsten-session';
app.post('/auth/change-password', requireAuth, async (req, res) => {
const hash = await hashPassword(req.body.newPassword);
await db.users.update({ id: req.userId }, { passwordHash: hash });
// Revoke all existing sessions — force re-login everywhere
await revokeAllUserSessions(req.userId, storage);
// Create a fresh session for the current device
const { sessionId } = await createSession(req.userId, { storage, ttl: '7d' });
res.cookie('sid', sessionId, { httpOnly: true, secure: true, sameSite: 'strict' });
res.json({ message: 'Password changed — other sessions revoked' });
});6. Privilege Escalation with Rotation
import { rotateSession } from '@periodic/tungsten-session';
app.post('/auth/sudo', requireAuth, async (req, res) => {
const isValid = await verifyPassword(req.body.password, req.user.passwordHash);
if (!isValid) return res.status(401).json({ error: 'Invalid password' });
// Rotate session ID to prevent fixation after privilege change
const { newSessionId } = await rotateSession(req.cookies.sid, { storage });
res.cookie('sid', newSessionId, { httpOnly: true, secure: true, sameSite: 'strict' });
res.json({ elevated: true });
});7. Structured Logging Integration
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
import { validateSession } from '@periodic/tungsten-session';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
async function validate(sessionId: string) {
const result = await validateSession(sessionId, { storage, extendOnAccess: true });
if (!result.valid) {
logger.warn('session.invalid', { reason: result.reason });
} else {
logger.info('session.valid', { userId: result.session.userId });
}
return result;
}8. Production Configuration
import { createSession, validateSession, revokeAllUserSessions } from '@periodic/tungsten-session';
const isDevelopment = process.env.NODE_ENV === 'development';
export const sessionConfig = {
storage,
ttl: isDevelopment ? '1h' : '7d',
idleTimeout: isDevelopment ? '15m' : '24h',
};
export const session = {
create: (userId: string) => createSession(userId, sessionConfig),
validate: (id: string) => validateSession(id, { ...sessionConfig, extendOnAccess: true }),
revokeAll: (userId: string) => revokeAllUserSessions(userId, storage),
};
export default session;🎛️ Configuration Options
createSession Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| storage | SessionStorage | required | Storage adapter |
| ttl | string \| number | required | Absolute expiry ('7d', '24h', '30m', or ms) |
| idleTimeout | string \| number | — | Idle timeout — reset on each valid access |
| metadata | Record<string, unknown> | — | Optional data stored with the session |
validateSession Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| storage | SessionStorage | required | Storage adapter |
| extendOnAccess | boolean | false | Reset idle timeout on each valid access |
rotateSession Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| storage | SessionStorage | required | Storage adapter |
| ttl | string \| number | — | New absolute TTL for the rotated session |
SessionValidationResult
| Field | Type | Description |
|-------|------|-------------|
| valid | boolean | Whether the session is valid |
| session | Session \| null | The session object (when valid: true) |
| reason | 'not_found' \| 'expired' \| 'idle_expired' | Why invalid (when valid: false) |
📋 API Reference
Session Functions
createSession(userId: string, options: CreateSessionOptions): Promise<{ sessionId: string; session: Session }>
validateSession(sessionId: string, options: ValidateSessionOptions): Promise<SessionValidationResult>
rotateSession(sessionId: string, options: RotateSessionOptions): Promise<{ newSessionId: string }>
revokeSession(sessionId: string, storage: SessionStorage): Promise<void>
revokeAllUserSessions(userId: string, storage: SessionStorage): Promise<number>Types
import type {
Session,
SessionStorage,
CreateSessionOptions,
ValidateSessionOptions,
SessionValidationResult,
} from '@periodic/tungsten-session';🧩 Architecture
@periodic/tungsten-session/
├── src/
│ ├── core/
│ │ ├── session.ts # createSession, validateSession, rotateSession, revokeSession, revokeAllUserSessions
│ │ └── types.ts # Session, SessionStorage, all options and result types
│ ├── utils/
│ │ ├── id.ts # Cryptographically secure session ID generation
│ │ ├── ttl.ts # TTL string parsing ('7d', '24h', '30m') → ms
│ │ └── time.ts # Clock abstraction for deterministic testing
│ └── index.ts # Public APIDesign Philosophy:
- Functions, not classes — each operation is an explicit function call, not a method on a stateful object
- Storage is injected — no global adapter, no singleton, safe for multi-tenant apps
- No framework coupling — works with Express, Fastify, Koa, or no framework at all
- No database opinions — the storage interface is four methods, nothing more
- Clock is injectable — pass a custom
clockfunction for deterministic testing
📈 Performance
- Single storage read per validation — no extra round trips for expiry checks
- Atomic rotation —
rotateSessioncreates and deletes in the correct order, no window of inconsistency - Timing-safe — session ID lookups use constant-time comparison, no timing side channels
- No global state — multiple configurations in the same process are fully isolated
- No monkey-patching — pure functions only, no prototype mutation
🚫 Explicit Non-Goals
This package intentionally does not include:
❌ JWT or token signing (use @periodic/tungsten)
❌ Browser-side session state (use @periodic/tungsten-client)
❌ Built-in Redis, Postgres, or DynamoDB adapters — bring your own
❌ Cookie management — set cookies yourself with your framework
❌ Rate limiting (use @periodic/titanium)
❌ Magic or implicit behavior on import
❌ Configuration files (configure in code)
Focus on doing one thing well: correct, storage-agnostic server-side session orchestration.
🎨 TypeScript Support
Full TypeScript support with complete type safety:
import type {
Session,
SessionStorage,
CreateSessionOptions,
ValidateSessionOptions,
SessionValidationResult,
} from '@periodic/tungsten-session';
// SessionValidationResult is discriminated — narrow by valid
const result = await validateSession(id, { storage });
if (result.valid) {
result.session.userId; // string — only present when valid
} else {
result.reason; // 'not_found' | 'expired' | 'idle_expired' — only when invalid
}
// Storage adapter is fully typed
const storage: SessionStorage = {
get: async (id) => { /* return Session | null */ },
set: async (session) => { /* store session */ },
delete: async (id) => { /* delete by id */ },
deleteByUserId: async (userId) => { /* return count deleted */ },
};🧪 Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchNote: All tests achieve >80% code coverage.
🤝 Related Packages
Part of the Periodic series by Uday Thakur:
- @periodic/tungsten - Authentication primitives (JWT, Argon2, TOTP, HMAC)
- @periodic/tungsten-client - Browser token state management
- @periodic/iridium - Structured logging
- @periodic/arsenic - Semantic runtime monitoring
- @periodic/zirconium - Environment configuration
- @periodic/vanadium - Idempotency and distributed locks
- @periodic/strontium - Resilient HTTP client
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
Build complete, production-ready APIs with the Periodic series!
📖 Documentation
🛠️ Production Recommendations
Environment Variables
SESSION_TTL=7d
SESSION_IDLE_TIMEOUT=24h
NODE_ENV=production
REDIS_URL=redis://localhost:6379Log Aggregation
Pair with @periodic/iridium for structured JSON output:
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
async function sessionMiddleware(req, res, next) {
const result = await validateSession(req.cookies.sid, { storage, extendOnAccess: true });
if (!result.valid) {
logger.warn('session.invalid', { reason: result.reason, ip: req.ip });
}
next();
}
// Pipe to Elasticsearch, Datadog, CloudWatch, etc.Security Monitoring
async function onPasswordChange(userId: string) {
const count = await revokeAllUserSessions(userId, storage);
Sentry.captureMessage('sessions.bulk_revocation', {
level: 'info',
extra: { userId, count },
});
}📝 License
MIT © Uday Thakur
🙏 Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details on:
- Code of conduct
- Development setup
- Pull request process
- Coding standards
- Architecture principles
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
🌟 Show Your Support
Give a ⭐️ if this project helped you build better applications!
Built with ❤️ by Uday Thakur for production-grade Node.js applications
