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

@restingowlorg/owlsessionguard

v1.0.0

Published

OwlSessionGuard: OWASP-aligned session management, rotation, revocation, and framework middleware for Node.js

Readme

OwlSessionGuard


npm package Node.js License

Open-source OWASP-aligned session management and security middleware for Node.js.

OwlSessionGuard, published as @restingowlorg/owlsessionguard, gives your backend a focused session-management surface: high-entropy session tokens, hash-only token storage, automatic rotation, reuse detection, device and IP binding, CSRF token binding, selective revocation, concurrent session limits, and framework adapters for Express, Fastify, and NestJS.

What You Get

  • Secure session lifecycle: Create, validate, rotate, revoke, revoke-all, and list user sessions through one typed service.
  • Hash-only token storage: Raw session tokens are returned once to the caller and are never persisted by the built-in stores.
  • Automatic reuse detection: Reuse of a rotated token can revoke the affected session tree.
  • Idle and absolute expiration: Support rolling idle timeouts and hard absolute session lifetime limits.
  • Security binding: Optional IP and device fingerprint checks with soft or hard enforcement modes.
  • CSRF protection: HMAC-signed CSRF tokens bound to the server-side session ID.
  • Concurrent session limits: Enforce global per-user caps and role-specific caps.
  • Storage adapters: In-memory storage for local/test use and Redis storage for production deployments.
  • Framework middleware: Express, Fastify, and NestJS integrations attach typed session context to requests.
  • Typed, predictable results: Public service methods return a consistent SessionOpResult<T> envelope.

Support Matrix

| Area | Current Support | | ------------- | -------------------------------------- | | Runtime | Node.js 18+ | | Language | TypeScript, JavaScript | | Module output | CommonJS | | Frameworks | Express, Fastify, NestJS | | Storage | In-memory, Redis | | Core flows | Create, Validate, Rotate, Revoke, List |

Installation

npm install @restingowlorg/owlsessionguard

Install only the framework peer dependencies you use:

npm install express
npm install fastify
npm install @nestjs/common @nestjs/core rxjs

Quick Start

import {
  SessionLibraryConfig,
  SessionReasonCode,
  SessionService,
} from "@restingowlorg/owlsessionguard";
import { MemoryStoreAdapter } from "@restingowlorg/owlsessionguard/storage/memory";

const store = new MemoryStoreAdapter();

const config: SessionLibraryConfig = {
  env: "production",
  transport: {
    mode: "cookie",
    cookie: {
      name: "__Host-session",
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      path: "/",
    },
  },
  expiration: {
    idleTimeoutSeconds: 60 * 60,
    absoluteTimeoutSeconds: 60 * 60 * 24,
    rolling: true,
  },
  rotation: {
    gracePeriodSeconds: 5,
  },
  security: {
    enforceTlsInProduction: true,
    ipBinding: "soft",
    fingerprinting: "hard",
    csrf: {
      enabled: true,
      secret: process.env.SESSION_CSRF_SECRET!,
      cookieName: "x-csrf-token",
      headerName: "x-csrf-token",
    },
  },
  device: {
    enabled: true,
    cookie: {
      name: "__Host-device",
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      path: "/",
    },
  },
  limits: {
    maxSessionsPerUser: 5,
    maxSessionsPerRole: {
      admin: 2,
    },
  },
  store: {
    provider: "memory",
  },
  observability: {
    debug: false,
    emitEvents: true,
    metrics: true,
  },
};

const sessions = new SessionService(store, config);

const created = await sessions.createSession({
  userId: "user_123",
  roles: ["admin"],
  scopes: ["read", "write"],
  metadata: {
    ipAddress: "203.0.113.10",
    userAgent: "Mozilla/5.0",
    deviceId: "device-cookie-value",
  },
});

if (!created.success) {
  throw new Error(created.error.message);
}

const { token, record } = created.data;

const validated = await sessions.validateSession({
  token,
  csrfToken: created.newCsrfToken,
  context: {
    ipAddress: "203.0.113.10",
    userAgent: "Mozilla/5.0",
    deviceFingerprint: record.metadata.deviceFingerprint,
    method: "POST",
  },
});

if (validated.success) {
  console.log(validated.data.userId);
}

await sessions.revokeSession({
  sessionId: record.id,
  reason: SessionReasonCode.MANUAL_LOGOUT,
});

Production note: Use MemoryStoreAdapter only for local development, tests, or single-process demos. Production deployments should use RedisStoreAdapter or a custom SessionStoreAdapter backed by durable infrastructure.

Storage

Memory

import { MemoryStoreAdapter } from "@restingowlorg/owlsessionguard/storage/memory";

const store = new MemoryStoreAdapter();

Redis

import Redis from "ioredis";
import { RedisStoreAdapter } from "@restingowlorg/owlsessionguard/storage/redis";

const redis = new Redis(process.env.REDIS_URL!);
const store = new RedisStoreAdapter(redis, {
  keyPrefix: "owlsessionguard:",
  ttlBufferSeconds: 60,
  batchSize: 500,
  maxAbsoluteTimeoutSeconds: 2592000,
});

| Option | Default | Purpose | | --------------------------- | ---------------- | ----------------------------------------------------------------------------------------- | | keyPrefix | "owlsessionguard:" | Redis key namespace prefix. | | ttlBufferSeconds | 0 | Seconds subtracted from session TTL for clock-drift safety. | | batchSize | 500 | Batch size for SCAN-based queries (e.g., findLiveSessionsForUser). | | maxAbsoluteTimeoutSeconds | 2592000 (30d) | Hard ceiling on absolute session lifetime in Redis, regardless of config. |

Framework Integration

Express

Peer dependency: express

import express from "express";
import {
  createExpressMiddleware,
  requireSession,
} from "@restingowlorg/owlsessionguard/express";

const app = express();

app.use(createExpressMiddleware(sessions, config));

app.get("/me", requireSession(), (req, res) => {
  res.json({
    userId: req.session?.userId,
    roles: req.session?.roles,
  });
});

Fastify

Peer dependency: fastify

import Fastify from "fastify";
import {
  fastifyRequireSession,
  fastifySessionPlugin,
} from "@restingowlorg/owlsessionguard/fastify";

const app = Fastify();

await app.register(fastifySessionPlugin, {
  service: sessions,
  config,
});

app.get("/me", { preHandler: fastifyRequireSession() }, async (request) => {
  return {
    userId: request.session?.userId,
    roles: request.session?.roles,
  };
});

NestJS

Peer dependencies: @nestjs/common, @nestjs/core, rxjs

// session.module.ts
import { Module } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { SessionService } from "@restingowlorg/owlsessionguard";
import { MemoryStoreAdapter } from "@restingowlorg/owlsessionguard/storage/memory";
import { SessionGuard, SessionInterceptor } from "@restingowlorg/owlsessionguard/nestjs";
import { AdminController } from "./admin.controller";

const store = new MemoryStoreAdapter();
const config = {
  env: "production" as const,
  transport: { mode: "cookie" as const },
  expiration: { idleTimeoutSeconds: 3600, absoluteTimeoutSeconds: 86400, rolling: true },
  rotation: { gracePeriodSeconds: 5 },
  security: {
    enforceTlsInProduction: true,
    ipBinding: "off" as const,
    fingerprinting: "off" as const,
    csrf: { enabled: false as const },
  },
  limits: { maxSessionsPerUser: 5 },
  store: { provider: "memory" as const },
  observability: { debug: false, emitEvents: false, metrics: false },
};

const sessionService = new SessionService(store, config);

@Module({
  controllers: [AdminController],
  providers: [
    { provide: SessionService, useValue: sessionService },
    Reflector,
    {
      provide: SessionGuard,
      inject: [SessionService, Reflector],
      useFactory: (service: SessionService, reflector: Reflector) =>
        new SessionGuard(service, config, reflector),
    },
    {
      provide: SessionInterceptor,
      inject: [SessionService],
      useFactory: (service: SessionService) =>
        new SessionInterceptor(service, config),
    },
  ],
})
export class SessionModule {}
// admin.controller.ts
import { Controller, Get, UseGuards } from "@nestjs/common";
import {
  RequireRoles,
  Session,
  SessionGuard,
} from "@restingowlorg/owlsessionguard/nestjs";
import type { SessionRecord } from "@restingowlorg/owlsessionguard";

@Controller("admin")
@UseGuards(SessionGuard)
export class AdminController {
  @Get("me")
  @RequireRoles("admin")
  getCurrentSession(@Session() session: SessionRecord | undefined) {
    return {
      userId: session?.userId,
      roles: session?.roles,
    };
  }
}

SessionGuard validates the session and enforces roles/scopes — it throws UnauthorizedException or ForbiddenException on failure. SessionInterceptor attaches the session optionally without throwing, useful when you want to read the session if present but still allow unauthenticated access to certain routes.

// profile.controller.ts — optional session attachment
import { Controller, Get, UseInterceptors } from "@nestjs/common";
import {
  Session,
  SessionInterceptor,
} from "@restingowlorg/owlsessionguard/nestjs";
import type { SessionRecord } from "@restingowlorg/owlsessionguard";

@Controller("profile")
@UseInterceptors(SessionInterceptor)
export class ProfileController {
  @Get("me")
  getProfile(@Session() session: SessionRecord | undefined) {
    if (!session) {
      return { anonymous: true };
    }
    return { userId: session.userId, roles: session.roles };
  }
}

Configuration Options

Core

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | env | "development" \| "test" \| "production" | Drives production-only security validation (e.g., rejects non-HTTPS cookies in production). |

Transport

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | transport.mode | "cookie" \| "header" \| "hybrid" | Selects how session tokens are read and written by middleware. | | transport.cookie.name | string | Session cookie name. Use __Host- prefix for host-bound cookies. | | transport.cookie.httpOnly | boolean | Prevents JavaScript access to the session cookie. | | transport.cookie.secure | boolean | Sends the cookie only over HTTPS. | | transport.cookie.signed | boolean | Signs the cookie with HMAC to detect tampering. | | transport.cookie.sameSite | "lax" \| "strict" \| "none" | CSRF protection mode for the cookie. | | transport.cookie.path | string | Cookie path scope. | | transport.cookie.domain | string | Cookie domain scope. | | transport.cookie.maxAgeSeconds | number | Maximum age of the cookie in seconds. | | transport.header.name | string | Request header name containing the session token. | | transport.header.scheme | string | Optional prefix stripped from the header value (e.g., Bearer). | | transport.header.responseHeader | string | Response header to write rotated tokens into. |

Expiration

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | expiration.idleTimeoutSeconds | number | Maximum inactivity period before a session expires. | | expiration.absoluteTimeoutSeconds | number | Hard maximum lifetime regardless of activity. | | expiration.rolling | boolean | Extends idle expiration on successful validation. |

Rotation

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | rotation.gracePeriodSeconds | number | Short retry window for in-flight requests after rotation. Maximum accepted value is 30 seconds. |

Security

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | security.enforceTlsInProduction | boolean | Rejects non-secure cookies when env is "production". | | security.ipBinding | "off" \| "soft" \| "hard" | Compares request IP address to the stored session IP. "soft" logs a warning; "hard" revokes the session. | | security.fingerprinting | "off" \| "soft" \| "hard" | Compares request device fingerprint to the stored session fingerprint. "soft" logs a warning; "hard" revokes the session. | | security.csrf.enabled | boolean | Enables HMAC-signed CSRF tokens bound to the session ID for mutating methods. | | security.csrf.secret | string | Minimum 32-character HMAC signing key. Required when enabled is true. | | security.csrf.previousSecret | string | Previous secret for gradual key rotation during secret rotation. | | security.csrf.cookieName | string | Cookie name for the CSRF token. | | security.csrf.headerName | string | Request header name for the CSRF token. |

Device

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | device.enabled | boolean | Enables persistent device-cookie extraction for fingerprint checks. | | device.cookie.name | string | Device cookie name. | | device.cookie.httpOnly | boolean | Prevents JavaScript access to the device cookie. | | device.cookie.secure | boolean | Sends the device cookie only over HTTPS. | | device.cookie.sameSite | "lax" \| "strict" \| "none" | CSRF protection mode for the device cookie. | | device.cookie.path | string | Device cookie path scope. | | device.cookie.domain | string | Device cookie domain scope. | | device.cookie.maxAgeSeconds | number | Maximum age of the device cookie in seconds. |

Limits

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | limits.maxSessionsPerUser | number | Maximum active sessions per user (global cap). | | limits.maxSessionsPerRole | Record<string, number> | Optional role-specific active-session limits. |

Concurrency

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | concurrency.lockTimeoutMs | number | Maximum time in ms to wait for a concurrency lock. Default: 50. | | concurrency.pollIntervalMs | number | Polling interval in ms when waiting for a lock. Default: 5. |

Store

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | store.provider | "memory" \| "redis" \| "mongo" \| "postgres" \| "custom" | Declares the configured storage backend. | | store.redis.url | string | Redis connection URL. Required when provider is "redis". | | store.redis.keyPrefix | string | Redis key prefix. Default: "owlsessionguard:". | | store.redis.ttlBufferSeconds | number | Seconds subtracted from TTL for clock-drift safety. Default: 0. | | store.mongo.uri | string | MongoDB connection URI. Required when provider is "mongo". | | store.mongo.collectionName | string | MongoDB collection name. | | store.postgres.url | string | PostgreSQL connection URL. Required when provider is "postgres". | | store.postgres.tableName | string | PostgreSQL table name. | | store.custom.adapter | SessionStoreAdapter | Custom store adapter instance. Required when provider is "custom". |

Note: "mongo" and "postgres" are accepted as config provider values for type safety and validation. Built-in adapters for these backends are not shipped in v1.0.0. Use store.provider: "custom" with a user-implemented SessionStoreAdapter for MongoDB or PostgreSQL.

Observability

| Option | Type | Purpose | | ----------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | observability.debug | boolean | Enables verbose debug logging for session operations. | | observability.emitEvents | boolean | Enables structured event listener dispatch. | | observability.metrics | boolean | Enables internal metrics counters. |

Session Operations

Rotate a Session

const rotated = await sessions.rotateSession({
  token,
  context: {
    ipAddress: "203.0.113.10",
    userAgent: "Mozilla/5.0",
    deviceFingerprint: record.metadata.deviceFingerprint,
    method: "POST",
  },
  reason: SessionReasonCode.ROTATION,
  roles: ["admin"],
});

if (rotated.success) {
  const nextToken = rotated.data.newToken;
}

Revoke All Sessions for a User

await sessions.revokeAllSessionsForUser(
  "user_123",
  SessionReasonCode.USER_ALL_SESSIONS_REVOKED,
);

List User Sessions

const listed = await sessions.listUserSessions("user_123", {
  limit: 20,
  role: "admin",
});

if (listed.success) {
  console.log(listed.data.sessions);
}

Selective Revocation

await sessions.selectiveRevocation.revokeByRole({
  userId: "user_123",
  role: "admin",
  reason: SessionReasonCode.ROLE_DEPRECATED,
});

Events

When observability.emitEvents is enabled, SessionService dispatches structured events. Subscribe with on() and unsubscribe with off():

sessions.on("session.created", (payload) => {
  console.log("Session created:", payload.sessionId, payload.userId, payload.timestamp);
});

sessions.on("session.rotated", (payload) => {
  console.log("Session rotated:", payload.oldSessionId, "→", payload.newSessionId);
});

sessions.on("security.rejection", (payload) => {
  console.warn("Security rejection:", payload.reason, payload.sessionId);
});

sessions.off("session.created", listener);

Available Events

| Event | Payload | Description | | ---------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------- | | session.created | { sessionId, userId, timestamp } | Fired after a new session is created. | | session.rotated | { oldSessionId, newSessionId, userId, timestamp } | Fired after a session token is rotated. | | session.revoked | { sessionId, userId, reason, timestamp } | Fired after a session is revoked. | | session.already_revoked | { sessionId, userId, timestamp } | Fired when revoking an already-revoked session. | | session.fallback_fingerprint | { sessionId, userId, deviceContext, timestamp } | Fired when a fallback fingerprint is used. | | sessions.listed | { userId, status, count, total, timestamp } | Fired after a session list query. | | security.rejection | { sessionId, userId, reason, message, affectedSessionIds, expectedIp, actualIp, expectedUserAgent, actualUserAgent, timestamp } | Fired when a security policy check fails. | | security.extractor_failed | { userId, error, metadata, timestamp } | Fired when device context extraction fails. | | batch.revoked | { target, timestamp, userId, reason, count, sessionIds, failedSessionIds } | Fired after a bulk selective revocation completes. | | internal_error | { context, errorClass, category, errorId, timestamp } | Fired on unexpected internal errors. |

Session Reason Codes

All SessionReasonCode enum values used to tag revocation and rotation reasons:

| Code | Value | Category | Description | | ---------------------------- | --------------------------- | ------------------ | ------------------------------------------------- | | MANUAL_LOGOUT | "manual_logout" | Normal | User explicitly logged out. | | ROTATION | "rotation" | Normal | Token was rotated (e.g., privilege change). | | IDLE_TIMEOUT | "idle_timeout" | Expiration | Session exceeded inactivity period. | | ABSOLUTE_TIMEOUT | "absolute_timeout" | Expiration | Session exceeded hard lifetime. | | ROTATION_GRACE_EXPIRED | "rotation_grace_expired" | Expiration | Grace window for in-flight requests expired. | | IP_MISMATCH | "ip_mismatch" | Security | Request IP does not match session IP. | | DEVICE_MISMATCH | "device_mismatch" | Security | Request device does not match session device. | | FINGERPRINT_MISMATCH | "fingerprint_mismatch" | Security | Request fingerprint does not match stored value. | | CSRF_VIOLATION | "csrf_violation" | Security | CSRF token validation failed. | | SECURITY_BREACH | "security_breach" | Security | Reuse of a rotated token detected. | | ADMIN_REVOKED | "admin_revoked" | Administrative | Administrator revoked the session. | | USER_ALL_SESSIONS_REVOKED | "user_all_sessions_revoked"| Administrative | All sessions for the user were revoked. | | DEVICE_LOGOUT | "device_logout" | Selective | All sessions on a specific device revoked. | | ROLE_DEPRECATED | "role_deprecated" | Selective | All sessions holding a specific role revoked. | | TIMESTAMP_PURGE | "timestamp_purge" | Selective | All sessions older than a timestamp revoked. |

Session Status

SessionStatus enum representing the lifecycle stage of a session:

| Status | Value | Description | | ---------- | ---------- | ------------------------------------------------------ | | ACTIVE | "active" | Session is valid and usable. | | ROTATED | "rotated"| Session was replaced by a new token (old token is in grace window). | | REVOKED | "revoked"| Session was explicitly revoked. | | EXPIRED | "expired"| Session expired (idle or absolute timeout). |

Header and Hybrid Transport

Header Mode

Use transport.mode: "header" to send session tokens via HTTP headers instead of cookies:

const config: SessionLibraryConfig = {
  // ...
  transport: {
    mode: "header",
    header: {
      name: "X-Session-Token",
      scheme: "Bearer",
      responseHeader: "X-New-Session-Token",
    },
  },
};

The client sends the token in the X-Session-Token header. On rotation, the new token is returned in the X-New-Session-Token response header.

Hybrid Mode

Use transport.mode: "hybrid" to accept tokens from both cookies and headers. The cookie is set on creation, but subsequent requests can provide the token via header:

const config: SessionLibraryConfig = {
  // ...
  transport: {
    mode: "hybrid",
    cookie: {
      name: "session",
      httpOnly: true,
      secure: true,
      sameSite: "lax",
    },
    header: {
      name: "X-Session-Token",
    },
  },
};

Custom Store Adapter

Implement the SessionStoreAdapter interface for custom storage backends:

import { SessionStoreAdapter } from "@restingowlorg/owlsessionguard/storage";
import { SessionRecord, SessionStatus, AdapterListParams, SessionListResult, SessionLimits, SessionReasonCode } from "@restingowlorg/owlsessionguard";

class PostgresStoreAdapter implements SessionStoreAdapter {
  constructor(private readonly pool: Pool) {}

  async create(record: SessionRecord, limits?: SessionLimits): Promise<void> {
    if (limits) {
      const count = await this.countActiveForUser(record.userId);
      if (count >= limits.maxSessionsPerUser) {
        throw new Error("SESSION_LIMIT_EXCEEDED");
      }
    }
    await this.pool.query(
      "INSERT INTO sessions (id, user_id, token_hash, status, roles, scopes, created_at, last_used_at, expires_at, idle_expires_at, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
      [record.id, record.userId, record.tokenHash, record.status, record.roles, record.scopes, record.createdAt, record.lastUsedAt, record.expiresAt, record.idleExpiresAt, JSON.stringify(record.metadata)],
    );
  }

  async rotate(oldId: string, newRecord: SessionRecord, oldUpdates: Partial<SessionRecord>, limits?: SessionLimits, oldRoles?: string[]): Promise<void> {
    // Atomic: update old session status, insert new session
    const client = await this.pool.connect();
    try {
      await client.query("BEGIN");
      await client.query("UPDATE sessions SET status = $1 WHERE id = $2", [oldUpdates.status, oldId]);
      await this.create(newRecord, limits);
      await client.query("COMMIT");
    } catch (e) {
      await client.query("ROLLBACK");
      throw e;
    } finally {
      client.release();
    }
  }

  async findById(id: string): Promise<SessionRecord | null> {
    const { rows } = await this.pool.query("SELECT * FROM sessions WHERE id = $1", [id]);
    return rows[0] ?? null;
  }

  async findByTokenHash(tokenHash: string): Promise<SessionRecord | null> {
    const { rows } = await this.pool.query("SELECT * FROM sessions WHERE token_hash = $1", [tokenHash]);
    return rows[0] ?? null;
  }

  async update(id: string, updates: Partial<SessionRecord>, limits?: SessionLimits): Promise<void> {
    const fields = Object.keys(updates);
    const values = Object.values(updates);
    const setClause = fields.map((f, i) => `${f} = $${i + 2}`).join(", ");
    await this.pool.query(`UPDATE sessions SET ${setClause} WHERE id = $1`, [id, ...values]);
  }

  async delete(id: string): Promise<void> {
    await this.pool.query("DELETE FROM sessions WHERE id = $1", [id]);
  }

  async deleteAllForUser(userId: string): Promise<void> {
    await this.pool.query("DELETE FROM sessions WHERE user_id = $1", [userId]);
  }

  async countActiveForUser(userId: string): Promise<number> {
    const { rows } = await this.pool.query(
      "SELECT COUNT(*)::int as count FROM sessions WHERE user_id = $1 AND status IN ('active', 'rotated')",
      [userId],
    );
    return rows[0].count;
  }

  async findAllForUser(userId: string, params?: AdapterListParams): Promise<SessionListResult> {
    let query = "SELECT * FROM sessions WHERE user_id = $1";
    const values: unknown[] = [userId];
    if (params?.status) {
      query += " AND status = $2";
      values.push(params.status);
    }
    query += " ORDER BY created_at DESC";
    const { rows } = await this.pool.query(query, values);
    return { sessions: rows, total: rows.length, totalIsApproximate: false, nextCursor: null };
  }
}

const store = new PostgresStoreAdapter(pool);
const config: SessionLibraryConfig = {
  // ...
  store: { provider: "custom", custom: { adapter: store } },
};

Response Model

Every public service method returns a discriminated result:

type SessionOpResult<T> =
  | {
      success: true;
      data: T;
      httpCode: number;
      newToken?: string;
      newCsrfToken?: string;
      clearCsrfToken?: boolean;
    }
  | {
      success: false;
      error: {
        code: string;
        message: string;
        reason?: SessionReasonCode;
      };
      httpCode: number;
      clearCsrfToken?: boolean;
    };

OWASP Alignment

OwlSessionGuard is not an OWASP certification and does not make an application compliant by itself. It implements session controls that map to OWASP guidance:

| Control | What the library does | OWASP reference | | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Session ID entropy | Generates 256-bit base64url session tokens with Node.js cryptographic randomness. | Session ID Entropy | | Protect session IDs | Stores token hashes only. Raw tokens are returned to the caller and should be transported in secure cookies or headers. | Protect Session IDs | | Renew session ID | Provides explicit rotation APIs for privilege changes and sensitive transitions. | Renew Session ID | | Reuse detection | Detects reuse of rotated tokens and can revoke the affected session tree. | Detect Session ID Anomalies | | CSRF tokens | Uses HMAC-signed CSRF tokens bound to the session ID for mutating methods. | CSRF Prevention |

Security Notes

The calling application remains responsible for:

  • Serving all authenticated traffic over HTTPS.
  • Authenticating the user before calling createSession.
  • Setting secure cookie attributes appropriate for the deployment.
  • Implementing route-level and resource-level authorization.
  • Protecting login, refresh, and session endpoints with rate limits.
  • Preventing XSS, since XSS can bypass many browser-side session protections.
  • Monitoring emitted events when observability.emitEvents is enabled.

Package Boundary

OwlSessionGuard owns server-side session state: session records, storage adapters, idle and absolute expiration, token rotation, revocation, reuse detection, security binding, and middleware session attachment.

Token signing, JWT verification, and access-token issuance belong in a token-management layer such as OwlTokenGuard or in your application. User authentication belongs in an authentication layer such as owlauth or in your application.

Community

Website LinkedIn GitHub Issues Security Policy Contributing

License

MIT License