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

unnbound-events

v3.1.3

Published

Workflow HTTP routing SDK with durable admission through the Temper gateway.

Readme

Unnbound Events SDK

HTTP workflow routing with cohort-specific delivery at the Temper gateway. Restate-admitted environments receive durable Runs over HTTP for wait/ack requests; legacy environments retain direct synchronous proxying and SQS asynchronous delivery. Stored passthrough routes and WebSocket upgrades do not create Runs.

c.request.body is always an ArrayBuffer. HTTP delivery preserves the exact request-content bytes after transport framing without content-type parsing or decompression; legacy SQS delivery retains its envelope re-serialization behavior described below.

Install

bun add unnbound-events

Quick Start

import { createServer } from 'unnbound-events';

const server = createServer();

// Register route handlers
server.post('/email/:userId', async (c) => {
  const { userId } = c.request.params;
  const payload = JSON.parse(Buffer.from(c.request.body).toString('utf8'));

  return { status: 200, body: { sent: true } };
});

// Start the HTTP listener
server.start();

The server automatically:

  • Starts HTTP server on PORT (default: 3000)
  • Provides /healthcheck endpoint (returns 200 { status: 'ok' })
  • Handles graceful shutdown on SIGINT/SIGTERM
  • Retains SQS polling for legacy queue-backed workflows: enabled by default when UNNBOUND_SQS_URL is present, with explicit queue.enabled overriding detection. Firecracker always disables queue polling, including when a stale queue URL is present.

Handlers

Routes receive a context object with request details. c.request.body is an ArrayBuffer containing the exact request-content bytes after HTTP transport framing, for every content type (zero-length when there is no body) — decode what you need inside the handler:

server.post('/users/:id', async (c) => {
  // Access request data
  const { id } = c.request.params;
  const query = c.request.query;
  const headers = c.request.headers;

  // Decode the raw bytes for the payload you expect:

  // JSON
  const payload = JSON.parse(Buffer.from(c.request.body).toString('utf8'));

  // Text
  const text = new TextDecoder().decode(c.request.body);

  // Multipart / form-data (boundary comes from the original content-type header)
  const form = await new Response(c.request.body, {
    headers: { 'content-type': c.request.headers['content-type'] },
  }).formData();

  // URL-encoded forms
  const params = new URLSearchParams(Buffer.from(c.request.body).toString('utf8'));

  // XML, EDI, CSV: use the library the workflow already depends on (xml2js, csv, @ontemper/edi)

  // Return response
  return {
    status: 200,
    body: { id },
    headers: { 'x-custom': 'value' },
  };
});

Transport framing and content encoding

c.request.body contains request content, not HTTP transport framing. Content encodings such as gzip are preserved, so decompress them explicitly when needed.

Response format: { status?: number, body?: object | Uint8Array | ArrayBuffer, headers?: Record<string, string> }

Objects are JSON-serialized. Uint8Array/ArrayBuffer bodies are sent verbatim as binary (set content-type explicitly) — required for protocols like AS2 whose responses must not be JSON-mangled.

Return undefined, null, or omit properties to default to 204 No Content.

Upgrading from 2.0.x

c.request.body used to vary by content-type. It is now always an ArrayBuffer of the exact request-content bytes after HTTP transport framing.

| content-type | 2.0.x | Current version | | ----------------------------------- | ------------- | --------------- | | application/json | parsed object | ArrayBuffer | | application/x-www-form-urlencoded | parsed object | ArrayBuffer | | multipart/form-data | parsed object | ArrayBuffer | | text/plain | string | ArrayBuffer | | anything else | ArrayBuffer | ArrayBuffer |

Handlers for the first four must decode explicitly — see Handlers for the per-type recipes. Handlers that already received bytes (XML, EDI, CSV, binary) are unaffected.

Three things to check when upgrading an existing workflow:

  • A malformed JSON body used to arrive as undefined. It now arrives as raw bytes and JSON.parse throws — handle it where you decode.
  • A missing body used to arrive as undefined, so if (!c.request.body) guards fired. Over HTTP it is now a zero-length ArrayBuffer, which is truthy, and those guards silently stop firing. Check c.request.body.byteLength instead. (For legacy SQS backlog drain during cutover, a bodyless async request replays as the four bytes null due to legacy queue envelope re-serialization).
  • tsc catches direct property access on the body, but not a body passed straight to a logger or serializer. Those compile clean and emit {} at runtime. Audit every server.post / server.put handler before deploying.

Middleware

Add logic that runs before/after handlers.

// Global middleware
server.use(async (c, next) => {
  console.log('Request:', c.request.method, c.request.path);
  return next();
});

// Path-based middleware
server.use('/admin/*', async (c, next) => {
  const token = c.request.headers['authorization'];
  if (!token?.startsWith('Bearer ')) {
    return { status: 401, body: { error: 'Unauthorized' } };
  }
  return next();
});

// Handler-specific middleware
server.post(
  '/email',
  async (c, next) => {
    // Runs only for this handler
    return next();
  },
  async (c) => {
    return { status: 200, body: { ok: true } };
  },
);

Configuration

HTTP Port

Set via PORT environment variable (default: 3000):

export PORT=8080

Durable admission

Use createEnqueuer to submit through the gateway's /async alias. The gateway acknowledges only after Restate accepts the Run. Payload storage and retries are platform responsibilities; the workflow SDK receives an ordinary HTTP request.

Logger Integration

Pass a custom logger:

import { logger } from 'unnbound-logger-sdk';

const server = createServer({
  logger,
});

The logger is used for internal events and traces.