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

@stefanobalocco/honosignedrequests

v1.3.0

Published

An hono middleware to manage signed requests, including a client implementation.

Downloads

97

Readme

@stefanobalocco/honosignedrequests

A Hono middleware for HMAC-SHA256 signed requests.

Overview

This library provides server-side session management and request signature validation for Hono applications, along with a client library for making signed requests.

Authentication Mechanism

Each session is associated with a cryptographic token (a random byte array) shared between client and server. Every request is authenticated by computing an HMAC-SHA256 signature using this token as the secret key.

The signature is computed over:

  • Session ID
  • Sequence number (monotonically increasing to prevent replay attacks)
  • Timestamp (to limit signature validity window)
  • Request parameters (sorted alphabetically)

The server validates the signature using the same token, verifies the timestamp falls within the allowed window, and checks that the sequence number is the expected next value for that session.

Features

  • HMAC-SHA256 request signing with shared secret token
  • Replay attack protection via monotonic sequence numbers
  • Timestamp validation with configurable tolerance, to prevent delayed replay
  • Constant-time signature comparison, to prevent timing attacks
  • Pluggable session storage architecture
  • Works with Node.js, Cloudflare Workers, Deno, and Bun

Installation

npm install @stefanobalocco/honosignedrequests

Server-Side Usage

Basic Setup

import { Hono } from 'hono';
import { SignedRequestsManager, SessionsStorageLocal } from '@stefanobalocco/honosignedrequests';

const app = new Hono();

// SessionsStorageLocal is a simple in-memory storage implementation
// You can implement your own SessionsStorage (e.g., Redis-based) for production
const storage = new SessionsStorageLocal({
  maxSessions: 65535,         // Storage-specific: maximum concurrent sessions in memory
  maxSessionsPerUser: 3       // Storage-specific: maximum sessions per user
});

// Generic parameters are passed to SignedRequestsManager
const signedRequests = new SignedRequestsManager(storage, {
  validitySignature: 5000,      // signature valid for 5 seconds
  validityToken: 3600000,       // session valid for 1 hour
  tokenLength: 32               // token size in bytes
});

app.use('/api/*', signedRequests.middleware);

Session Creation (Login Endpoint)

app.post('/auth/login', async (c) => {
  const { username, password } = await c.req.json();
  
  // Your authentication logic here
  const userId = await authenticateUser(username, password);
  
  if (!userId) {
    return c.json({ error: 'Invalid credentials' }, 401);
  }
  
  // Use the manager's createSession method
  const session = await signedRequests.createSession(userId);
  
  // Convert token to Base64URL for transmission to client
  const tokenBase64 = btoa(String.fromCharCode(...session.token))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
  
  return c.json({
    sessionId: session.id,
    token: tokenBase64,
    sequenceNumber: session.sequenceNumber
  });
});

Ping Endpoint (Verify Authentication)

app.post('/api/ping', (c) => {
  const session = c.get('session');
  return c.json({ pong: !!session });
});

Protected Endpoint

app.post('/api/protected', (c) => {
  const session = c.get('session');
  
  if (!session) {
    return c.json({ error: 'Unauthorized' }, 401);
  }
  
  return c.json({
    message: 'Authenticated',
    userId: session.userId
  });
});

Configuration

The library separates generic parameters (common to all storage implementations) from storage-specific parameters.

Generic Parameters (SignedRequestsManagerConfig)

These are passed to SignedRequestsManager constructor and apply to all storage implementations:

| Option | Default | Description | |--------|---------|-------------| | validitySignature | 5000 | Signature validity window in milliseconds | | validityToken | 3600000 | Session token validity in milliseconds | | tokenLength | 32 | Token length in bytes (cryptographic secret) | | onError | undefined | Callback invoked when an error occurs during request validation |

SessionsStorageLocal Specific Parameters

These are specific to the in-memory implementation:

| Option | Default | Description | |--------|---------|-------------| | maxSessions | 65535 | Maximum concurrent sessions in memory | | maxSessionsPerUser | 3 | Maximum sessions per user (enforced by removing oldest) |

Client-Side Usage

Browser Import (CDN)

<script type="module">
  import SignedRequester from 'https://cdn.jsdelivr.net/gh/StefanoBalocco/[email protected]/client/dist/SignedRequester.min.js';
  // Named import also works:
  // import { SignedRequester } from 'https://cdn.jsdelivr.net/gh/StefanoBalocco/[email protected]/client/dist/SignedRequester.min.js';
  
  const requester = new SignedRequester();
  // You can also specify a base URL for requests
  //const requester = new SignedRequester('https://api.example.com');
  // You can also specify an error handler for encoding/decoding errors
  //const requester = new SignedRequester('https://api.example.com', (error) => console.error(error));
  
  // Check if we have session data stored
  let needLogin = true;
  if (requester.getSession()) {
    // Try to verify the session is still valid
    try {
      const response = await requester.signedRequestJson('/api/ping', {});
      
      if (response?.pong) {
        needLogin = false;
      }
    } catch (error) {
    }
  }
  
  if( needLogin ) {
    // No session data, need to login
		const response = await fetch('/auth/login', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({
				username: '[email protected]',
				password: 'password123'
			})
		});

		const loginData = await response.json();

		// Store session credentials
		requester.setSession({
			sessionId: loginData.sessionId,
			token: loginData.token,
			sequenceNumber: loginData.sequenceNumber
		});
  }
  
  // Now we're authenticated, make protected requests
  const data = await requester.signedRequestJson('/api/protected', {
    action: 'getData'
  });
</script>

Client API

Constructor

const requester = new SignedRequester(baseUrl?, onError?);

| Parameter | Type | Description | |-----------|------|-------------| | baseUrl | string | Optional base URL for all requests. If not provided, relative paths are used. | | onError | (error: unknown) => void | Optional callback invoked when encoding/decoding errors occur. |

setSession(config)

Initialize session after login.

requester.setSession({
  sessionId: 12345,
  token: 'base64url_encoded_token',
  sequenceNumber: 1
});

getSession()

Check if a valid session exists. Loads from localStorage if not in memory.

if (requester.getSession()) {
  // Session available
}

signedRequest(path, parameters, options?)

Make a signed request, returns the raw Response object.

const response = await requester.signedRequest('/api/action', {
  param1: 'value1',
  param2: 123
});

signedRequestJson<T>(path, parameters, options?)

Make a signed request and parse JSON response.

const data = await requester.signedRequestJson('/api/data', {
  query: 'example'
});

clearSession()

Clear session data (for example after you do a logout).

requester.clearSession();

Request Options

{
  baseUrl?: string;           // Override base URL for this request
  headers?: Record<string, string>;  // Additional headers
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';  // HTTP method (default: POST)
}

Signature Format

The signature is computed over the following concatenated string:

sessionId={id};sequenceNumber={seq};timestamp={ts};{sorted_params}

The HMAC-SHA256 signature is computed using the session token as the secret key.

Parameters are sorted alphabetically by key. Values are serialized as:

  • Primitives (string, number, boolean) and null: String(value)
  • Objects and arrays: JSON.stringify(value)

Implementing Custom Storage

To implement your own session storage (e.g., Redis-based), extend the SessionsStorage abstract class:

import { SessionsStorage } from '@stefanobalocco/honosignedrequests';
import { Session } from '@stefanobalocco/honosignedrequests';

class RedisSessionsStorage extends SessionsStorage {
  async create(
    validityToken: number,
    tokenLength: number,
    userId: number
  ): Promise<Session> {
    // Implement session creation with Redis
    // Generate token with specified tokenLength
    // Use validityToken for Redis TTL or expiration tracking
    // Implement your own maxSessionsPerUser logic if needed
  }

  async getBySessionId(sessionId: number): Promise<Session | undefined> {
    // Implement session lookup with Redis
  }

  async getByUserId(userId: number): Promise<Session[]> {
    // Implement session lookup with Redis
  }

  async delete(sessionId: number): Promise<void> {
    // Implement session deletion with Redis
  }
}

Considerations

  • Storage should expire sessions
  • Invalid sessions should return 401 and trigger client-side session clearing
  • Client should be forced to serialize requests, otherwise out-of-order sequence number may arise triggering a session cleanup.

License

BSD-3-Clause