@devcoons/redis-session-store
v0.1.0
Published
> A Redis-backed session lineage manager for refresh token rotation, revocation, and device-level logout.
Readme
redis-session-store
A Redis-backed session lineage manager for refresh token rotation, revocation, and device-level logout.
Overview
redis-session-store provides a minimal, atomic, and framework-agnostic session store for modern authentication systems.
It is designed to manage rotating refresh tokens, device binding, and secure logout semantics - backed by Redis and Lua for atomicity.
Key design principles:
- ⚙️ Atomic operations (via Lua)
- 🔗 Lineage tracking (
rid → parentRid) - 💻 Device and user-agent binding
- ⏱️ Hard absolute TTLs for security
- 🚪 Explicit logout (single, device, or all sessions)
- 🧹 Garbage collection of orphaned or expired RIDs
Installation
# npm
npm install @devcoons/redis-session-store
# or pnpm
pnpm add @devcoons/redis-session-storeBasic Usage
import { createRedisStore } from '@devcoons/redis-session-store';
const store = createRedisStore({
url: process.env.REDIS_URL ?? 'redis://localhost:6379',
absTtlSeconds: 30 * 24 * 3600, // 30 days
});
// Issue an initial session lineage
const session = await store.issueInitial('user-123', {
now: Math.floor(Date.now() / 1000),
deviceId: 'chrome-laptop',
uaHash: 'abc123',
ipHash: 'xyz789',
absTtlSeconds: 30 * 24 * 3600,
});
// Verify and rotate a refresh token
const rotated = await store.verifyAndRotateRefresh({
rid: session.rid,
now: Math.floor(Date.now() / 1000) + 3600,
deviceId: 'chrome-laptop',
uaHash: 'abc123',
ipHash: 'xyz789',
});
console.log('New RID:', rotated.newRid);
// Logout a single session
await store.logout(session.rid);
// Logout all sessions for this user
await store.logoutAll('user-123');
// Garbage collect expired or orphaned sessions
await store.gcOrphans({ graceSeconds: 60 });API Reference
createRedisStore(config: Cfg): SessionStore
Creates a Redis-backed store instance.
type Cfg = {
url: string; // Redis connection URL
prefix?: string; // Default: "auth"
absTtlSeconds: number; // Hard expiry for session lineage
};Interface: SessionStore
export interface SessionStore {
issueInitial(
userId: UserId,
input: Omit<VerifyRotateInput, 'rid'> & { absTtlSeconds: number }
): Promise<RefreshIssue>;
verifyAndRotateRefresh(
input: VerifyRotateInput
): Promise<{ userId: UserId; newRid: RefreshIssue }>;
tombstoneLineage(rid: string): Promise<void>;
logout(rid: string): Promise<void>;
logoutDevice(userId: string, deviceId: string): Promise<void>;
logoutAll(userId: UserId): Promise<void>;
getRid(rid: string): Promise<Record<string, unknown> | null>;
scanRids(userId: UserId): Promise<string[]>;
gcOrphans(opts?: { dryRun?: boolean; graceSeconds?: number }): Promise<GCReport>;
}Supporting Types
type UserId = string;
interface VerifyRotateInput {
rid: string;
now: number;
deviceId?: string;
uaHash?: string;
ipHash?: string;
}
interface RefreshIssue {
rid: string;
absExp: number;
}
interface GCReport {
scannedRids: number;
removedRids: number;
removedFromSets: number;
dryRun?: boolean;
}API Methods Overview
| Method | Description |
|--------|--------------|
| issueInitial(userId, input) | Creates a new refresh lineage (first RID) for a user. |
| verifyAndRotateRefresh(input) | Atomically verifies and rotates an existing refresh RID, returning the new one. |
| tombstoneLineage(rid) | Marks a lineage as inactive, used internally when invalidating refresh chains. |
| logout(rid) | Logs out a single session (marks the RID tombstoned and removes it from its user set). |
| logoutDevice(userId, deviceId) | Logs out all sessions linked to a specific device ID. |
| logoutAll(userId) | Logs out all sessions belonging to a given user. |
| getRid(rid) | Returns the stored metadata for a given RID, or null if missing. |
| scanRids(userId) | Returns all active RIDs for the user. |
| gcOrphans(opts) | Removes expired or orphaned RIDs from Redis safely. |
Redis Data Model
| Key | Type | Description |
|-----|------|--------------|
| auth:rid:<rid> | Hash | Stores session info and lineage metadata |
| auth:user:<userId>:rids | Set | Tracks all RIDs belonging to a user |
| auth:device:<userId>:<deviceId> | Set | Tracks RIDs associated with a specific device |
| auth:... | Configurable prefix via prefix option |
RID Hash Fields
| Field | Meaning |
|--------|----------|
| userId | Owner of the session |
| parentRid | Previous RID (for lineage tracking) |
| createdAt | Creation time (epoch seconds) |
| expAt | Rolling expiration (soft cap) |
| absExpAt | Absolute expiration (hard cap) |
| deviceId / uaHash / ipHash | Device binding info |
| status | "active", "used", or "tombstoned" |
Garbage Collection
Call periodically to clean up expired or orphaned entries:
await store.gcOrphans({ graceSeconds: 60 });Or dry-run mode for inspection:
const report = await store.gcOrphans({ dryRun: true });
console.log(report);Recommended frequency: every hour via cron or worker.
Development
Start Redis with Docker:
pnpm dev:upInspect with RedisInsight at:
localhost:6379Debug commands:
pnpm redis:issue
pnpm redis:get <rid>
pnpm redis:rotate <rid>
pnpm redis:gcShutdown Redis:
pnpm dev:downLicense
MIT © 2025 Io .D (devcoons)
