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

@pronghorn/session

v0.1.1

Published

Server-side session middleware for Pronghorn. Stores session data behind a signed session-ID cookie with a pluggable store, defaulting to in-memory.

Readme

Session 🔑

Session is a lightweight, TypeScript-first server-side session middleware built as an external plugin for Pronghorn. It stores arbitrary session data behind a signed session-ID cookie, backed by a pluggable store, and exposes a mutable context.locals.session object that persists automatically after each request.

Built as a standalone package (@pronghorn/session), layers directly on top of @pronghorn/cookie's signing primitives, the missing piece between stateless JWT auth and full server-side session state.

Why Session

@pronghorn/cookie can read and write signed cookies, but every value still round-trips to the client, fine for small flags, unsuitable for larger or sensitive session state like cart contents, flash messages, or server-verified login state. Session solves this by keeping the actual data server-side, and only ever sending a signed, opaque session ID to the browser.

  • Session data lives entirely server-side behind a pluggable SessionStore, only a signed session ID touches the client.
  • Ships with a default in-memory store, swap in Redis, Prisma, or any custom store by implementing three async methods.
  • New sessions are issued automatically on first use, no manual cookie bootstrapping required.
  • regenerate() support for session-fixation-safe login flows (rotate the ID, keep or reset the data).
  • rolling expiration extends session lifetime on every request, or set false for a fixed absolute expiry.
  • Built directly on @pronghorn/cookie's HMAC signing and timing-safe verification, no duplicated crypto logic.

Installation

bun add @pronghorn/session @pronghorn/cookie

Requires Bun >=1.3.0 and pronghorn >=0.1.2 as a peer dependency (used for typing the middleware only). @pronghorn/cookie is a direct runtime dependency, its signing and parsing primitives are reused rather than reimplemented.

Quick Start

import { createApp } from 'pronghorn'
import { session } from '@pronghorn/session'

const app = createApp()

app.use(session({ secret: process.env.SESSION_SECRET ?? 'dev-secret', maxAge: 3600 }))

app.post('/login', context => {
  const userSession = context.locals.session as Session
  userSession.data.userId = 42
  return context.json({ loggedIn: true })
})

app.get('/me', context => {
  const userSession = context.locals.session as Session
  return context.json({ userId: userSession.data.userId ?? null })
})

await app.listen(4000)

Core Concepts

Reading and writing session data

Once session() is registered, context.locals.session.data is a plain mutable object. Any change made to it during the request is automatically persisted to the store afterward, no explicit "save" call needed.

app.post('/cart', context => {
  const userSession = context.locals.session as Session
  const items = (userSession.data.items as string[]) ?? []
  items.push('widget-42')
  userSession.data.items = items
  return context.json({ items })
})

First-time sessions

If no valid session cookie is present (or the signature fails verification), Session transparently creates a new empty session and issues a fresh signed cookie in the response, your handler never needs to check for this case explicitly.

app.get('/visits', context => {
  const userSession = context.locals.session as Session
  const visits = ((userSession.data.visits as number) ?? 0) + 1
  userSession.data.visits = visits
  return context.json({ visits }) // starts at 1 on first request, increments after
})

Destroying a session

session.destroy() clears the store entry and empties the in-request data object, use this for logout flows.

app.post('/logout', async context => {
  const userSession = context.locals.session as Session
  await userSession.destroy()
  return context.json({ loggedOut: true })
})

Regenerating a session ID

session.regenerate() rotates the session ID while keeping the middleware's response flow intact, useful right after a successful login to prevent session-fixation attacks.

app.post('/login', async context => {
  const userSession = context.locals.session as Session
  await userSession.regenerate()
  userSession.data.userId = 42
  return context.json({ loggedIn: true })
})

Rolling vs. fixed expiration

By default (rolling: true), a session's expiration is pushed forward on every request, keeping active users logged in indefinitely. Set rolling: false for a fixed expiry from creation time regardless of activity.

app.use(session({ secret: process.env.SESSION_SECRET!, maxAge: 1800, rolling: false }))

Middleware Options

| Option | Type | Default | Description | | ------------ | -------------- | --------------------- | ----------------------------------------------------------- | | secret | string | - | Required. HMAC signing secret for the session-ID cookie | | store | SessionStore | createMemoryStore() | Backing store for session data | | cookieName | string | 'sid' | Name of the session-ID cookie | | maxAge | number | 86400 (1 day) | Session lifetime in seconds | | rolling | boolean | true | Extends expiration on every request when true |

Custom Stores

Implement SessionStore to back sessions with Redis, a database, or any storage layer. Only three async methods are required.

import type { SessionStore, SessionRecord } from '@pronghorn/session'

function createRedisStore(client: RedisClient): SessionStore {
  return {
    async get(sessionId) {
      const raw = await client.get(`session:${sessionId}`)
      return raw ? (JSON.parse(raw) as SessionRecord) : null
    },
    async set(sessionId, record) {
      const ttlSeconds = Math.max(1, Math.floor((record.expiresAt - Date.now()) / 1000))
      await client.set(`session:${sessionId}`, JSON.stringify(record), { EX: ttlSeconds })
    },
    async destroy(sessionId) {
      await client.del(`session:${sessionId}`)
    }
  }
}

app.use(session({ secret: process.env.SESSION_SECRET!, store: createRedisStore(redis) }))

API Reference

session(options: SessionOptions): Middleware - global middleware factory, register via app.use(session(options)).

| Locals property | Type | Description | | ------------------------ | --------- | ------------------------------------------------- | | context.locals.session | Session | The active session handle for the current request |

Session

| Property/Method | Type | Description | | --------------- | ------------------------- | ------------------------------------------------------------------------ | | id | string | The current (unsigned) session ID | | data | Record<string, unknown> | Mutable session data, persisted automatically after the handler resolves | | destroy() | Promise<void> | Clears the store entry and empties data | | regenerate() | Promise<void> | Rotates the session ID, destroying the old store entry |

SessionStore

| Method | Signature | Description | | --------- | ------------------------------------------------------------- | ---------------------------------------------------- | | get | (sessionId: string) => Promise<SessionRecord \| null> | Loads a session record, or null if missing/expired | | set | (sessionId: string, record: SessionRecord) => Promise<void> | Persists a session record | | destroy | (sessionId: string) => Promise<void> | Removes a session record |

SessionRecord is { data: Record<string, unknown>; expiresAt: number }.

Architecture

Session is split into two modules on top of @pronghorn/cookie's exported primitives.

| Module | Responsibility | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | memory-store.ts | Default SessionStore implementation using an in-memory Map, with lazy expiration checks on read | | middleware.ts | Resolves the session ID from a signed cookie, loads/creates the record, exposes the mutable Session handle, and persists + re-signs the cookie after the handler resolves |

Session ID signing and verification reuse @pronghorn/cookie's signValue/unsignValue (HMAC-SHA256 with timing-safe comparison), rather than reimplementing cryptographic logic in a second package.

Security Notes

  • Always use a long, random secret, and never commit it to source control, load it from an environment variable.
  • The default in-memory store does not survive process restarts and does not scale across multiple instances, use a shared store (Redis, database) in any multi-instance deployment.
  • Call regenerate() immediately after a successful login to prevent session-fixation attacks, where an attacker pre-sets a victim's session ID before authentication.
  • The session cookie is set with httpOnly: true by default, preventing client-side JavaScript from reading the raw session ID.

License

WTFPL (Do What the Fuck You Want to Public License), see LICENSE for details.