npm package: @jsleekr/hookbox
Description: <div align="center">
Latest version: 0.4.0

<div align="center">

# 📦 hookbox

### Capture, inspect, and replay webhooks locally

[![GitHub Stars](https://img.shields.io/github/stars/JSLEEKR/hookbox?style=for-the-badge&logo=github&color=yellow)](https://github.com/JSLEEKR/hookbox/stargazers)
[![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.4-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](#)
[![Tests](https://img.shields.io/badge/tests-122%20passing-brightgreen?style=for-the-badge)](#)

<br/>

**Local webhook development proxy -- no SaaS required.**

Capture + Inspect + Replay + Mock

[Quick Start](#quick-start) | [Features](#features)

</div>

---

## Why This Exists

Debugging webhooks is painful when you depend on cloud tunnels and third-party dashboards. hookbox runs entirely on your machine -- your payloads never leave. Zero config gets you capturing in one command, and every request is persisted to SQLite so you can replay it hours later to reproduce a bug. Mock responses let you test your integration without a live webhook provider even being involved.

## Features

| Feature | Detail |
|---|---|
| HTTP proxy capture | All methods, headers, body, query params saved to SQLite |
| Request replay | Forward any saved request to a target URL |
| Retry with backoff | Configurable exponential backoff on replay failures |
| Mock responses | Route-pattern-matched mock rules with custom status/headers/body; persisted to `~/.hookbox/mocks.json` |
| Webhook signature verification | GitHub (HMAC-SHA256) and Stripe (timestamped v1) |
| Stripe timestamp tolerance | Reject replayed webhooks older than 5 minutes (configurable) |
| LRU eviction | `--max-requests` enforces a hard cap; oldest requests evicted automatically |
| Body size limit | Requests exceeding 10 MB are rejected to prevent OOM |
| Hop-by-hop header stripping | `connection`, `transfer-encoding`, `host`, etc. removed before replay |
| Fetch timeout | 30-second timeout on all outbound forwarding and replay calls |
| Request tagging | Attach arbitrary string tags; filter and list by tag |
| Search | Full-text search across body and header key/value |
| Diff | Side-by-side comparison of two captured requests |
| CSV export (RFC 4180) | Fields with commas, quotes, or newlines are correctly escaped |
| HAR export | Standard HTTP Archive format for import into browser tools |
| CLI formatters | `prettyPrintBody`, `diffRequests`, `exportToHar` extracted to `src/core/formatters.ts` |
| SQLite foreign keys | Tag rows cascade-delete when parent request is deleted |
| Middleware hooks | Register `before`/`after` hooks to transform or observe requests |
| Request statistics | `hookbox stats` — total, by-method, by-content-type, avg size |
| JSDoc documentation | Every public class and method has JSDoc comments |

## Quick Start

```bash
# Install globally (or use npx hookbox)
npm install -g hookbox

# Start capturing webhooks on port 3000
hookbox start --port 3000

# Forward captured webhooks to your local app
hookbox start --port 3000 --target http://localhost:8080

# List captured requests
hookbox list

# Show full details of a request
hookbox show <request-id>

# Replay a request to your app
hookbox replay <request-id> --target http://localhost:8080

# For local development from source:
#   npm run build && node dist/cli/index.js start --port 3000
```

## Commands

### `hookbox start`

Start the webhook proxy server.

```bash
hookbox start [options]
  -p, --port <port>           Port to listen on (default: 3000)
  -t, --target <url>          Forward requests to this URL
  -d, --db <path>             Database file path (default: ./hookbox.db)
  -m, --max-requests <count>  Maximum requests to store (default: 10000)
```

### `hookbox list`

List captured requests in a table or JSON format.

```bash
hookbox list [options]
  -l, --limit <count>   Max requests to show (default: 20)
  -m, --method <method> Filter by HTTP method (GET, POST, etc.)
  -p, --path <path>     Filter by path
  -t, --tag <tag>       Filter by tag
  --json                Output as JSON
  -d, --db <path>       Database file path
```

### `hookbox show <id>`

Show full details of a captured request including headers, body (pretty-printed), and metadata.

```bash
hookbox show <id> [options]
  --json          Output as JSON
  -d, --db <path> Database file path
```

### `hookbox replay <id>`

Replay a captured request to a target URL.

```bash
hookbox replay <id> [options]
  -t, --target <url>  Target URL (required)
  --retry <count>     Retry with exponential backoff
  --json              Output as JSON
  -d, --db <path>     Database file path
```

### `hookbox mock`

Manage mock response rules.

```bash
# Add a mock rule
hookbox mock add --path /webhook --status 200 --body '{"ok":true}'
hookbox mock add --path /webhook/* --method POST --status 201

# List mock rules
hookbox mock list

# Remove a mock rule
hookbox mock remove <rule-id>
```

Mock rules are persisted to `~/.hookbox/mocks.json` and automatically loaded when the server starts.

### `hookbox stats`

Show aggregate request statistics.

```bash
hookbox stats [options]
  --json          Output as JSON
  -d, --db <path> Database file path
```

### `hookbox search`

Search captured requests by body content or headers.

```bash
hookbox search [options]
  -b, --body <text>          Search in request body
  -k, --header-key <key>     Search by header key
  -v, --header-value <value> Search by header value
  --json                     Output as JSON
  -d, --db <path>            Database file path
```

### `hookbox diff <id1> <id2>`

Compare two captured requests side-by-side showing differences in method, path, headers, and body.

### `hookbox tag <action> <id> [tag]`

Manage request tags for organization.

```bash
hookbox tag add <id> stripe      # Add tag
hookbox tag remove <id> stripe   # Remove tag
hookbox tag list <id>            # List tags
```

### `hookbox clear`

Delete all captured requests.

### `hookbox export`

Export captured requests in JSON, CSV, or HAR format.

```bash
hookbox export [options]
  -f, --format <format>  Output format: json, csv, har (default: json)
  -d, --db <path>        Database file path
```

## Recipes

### GitHub Push Webhook

Capture and verify GitHub push events:

```bash
# Start proxy to capture GitHub webhooks
hookbox start --port 3000 --target http://localhost:8080

# Configure your GitHub repo webhook URL to http://<your-host>:3000/webhook/github

# After receiving webhooks, verify signatures programmatically:
```

```typescript
import { RequestStore, verifyGitHubSignature } from 'hookbox';

const store = new RequestStore('./hookbox.db');
const requests = store.list({ path: '/webhook/github' });

for (const req of requests) {
  const valid = verifyGitHubSignature(
    req.body,
    req.headers['x-hub-signature-256'],
    process.env.GITHUB_WEBHOOK_SECRET!,
  );
  console.log(`${req.id}: signature ${valid ? 'VALID' : 'INVALID'}`);
}
store.close();
```

### Stripe Payment Webhook

Capture Stripe events with signature verification and mock responses for testing:

```bash
# Mock Stripe's expected 200 OK response during development
hookbox mock add --path /webhook/stripe --method POST --status 200 --body '{"received":true}'

# Start proxy — matching requests get the mock response while still being captured
hookbox start --port 3000

# List only Stripe webhooks
hookbox list --path /webhook/stripe

# Replay a charge.succeeded event to your handler
hookbox replay <request-id> --target http://localhost:8080 --retry 3
```

```typescript
import { verifyStripeSignature } from 'hookbox';

// Verify in your handler
const valid = verifyStripeSignature(
  rawBody,
  headers['stripe-signature'],
  process.env.STRIPE_WEBHOOK_SECRET!,
);
```

### Slack Events API

Capture and replay Slack events with URL verification handling:

```bash
# Mock the URL verification challenge response Slack expects
hookbox mock add --path /webhook/slack --method POST --status 200 \
  --body '{"challenge":"mock-challenge-token"}' \
  --header 'Content-Type:application/json'

# Start capturing
hookbox start --port 3000

# Search for specific Slack event types
hookbox search --body '"type":"event_callback"'

# Tag interesting events for later review
hookbox tag add <request-id> slack-event

# Export all Slack events for analysis
hookbox export --format har > slack-events.har
```

## How It Works

```
Incoming HTTP Request
        |
        v
  ProxyServer (src/core/proxy.ts)
        |
        |-- Body read (max 10 MB enforced)
        |-- Before hooks run
        |-- MockEngine.match() checked
        |     |
        |     yes --> return mock response
        |     no  --> continue
        |
        |-- RequestStore.save() → SQLite (WAL mode, foreign keys ON)
        |-- After hooks run
        |
        |-- target URL set?
              |
              yes --> fetch(targetUrl, { signal: AbortSignal.timeout(30000) })
              |       strip hop-by-hop headers (Replayer)
              |       return upstream response
              no  --> 200 { captured: true, id }

CLI (src/cli/index.ts)
  |
  ├── list / show / search / diff / tag
  ├── replay  ──→  Replayer (retry + backoff, 30s timeout, hop-by-hop strip)
  ├── mock    ──→  MockEngine (persisted to ~/.hookbox/mocks.json)
  ├── stats   ──→  RequestStore.getStats()
  ├── export  ──→  JSON | CSV (RFC 4180) | HAR
  └── clear

Core modules
  ├── src/core/proxy.ts       — HTTP server + request capture pipeline
  ├── src/core/store.ts       — SQLite persistence via better-sqlite3
  ├── src/core/replayer.ts    — Replay + retry/backoff + header stripping
  ├── src/core/mock.ts        — Mock rule engine
  ├── src/core/signature.ts   — GitHub + Stripe signature verification
  ├── src/core/formatters.ts  — prettyPrintBody, diffRequests, exportToHar
  └── src/utils.ts            — CSV field escaping (RFC 4180)
```

## Security

### Body Size Limit

The proxy enforces a **10 MB maximum** on incoming request bodies. Requests exceeding this limit are rejected immediately and the connection is destroyed, preventing out-of-memory attacks.

```typescript
export const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
```

### Header Masking

Sensitive headers such as `Authorization` are stored as-is in the local SQLite database (local-only, under your control). The replay path strips hop-by-hop headers (`connection`, `keep-alive`, `transfer-encoding`, `host`, `content-length`, etc.) before forwarding to protect upstream services from protocol-level issues.

### Webhook Signature Verification

```typescript
import { verifyGitHubSignature, verifyStripeSignature } from 'hookbox';

// GitHub — HMAC-SHA256 over raw body
const isValid = verifyGitHubSignature(
  requestBody,                        // Raw request body string
  headers['x-hub-signature-256'],     // SHA-256 signature header
  'your-webhook-secret'               // Your webhook secret
);

// Stripe — timestamped v1 signature, 5-minute tolerance by default
const isValid = verifyStripeSignature(
  requestBody,                        // Raw request body string
  headers['stripe-signature'],        // t=...,v1=... header
  'whsec_your_endpoint_secret',       // Your endpoint secret
  300,                                // Tolerance in seconds (default: 300)
);
```

Both functions use `timingSafeEqual` for constant-time comparison to prevent timing attacks. The Stripe verifier rejects signatures with a timestamp older than the configured tolerance (default **5 minutes / 300 seconds**), matching Stripe's own SDK behavior.

## Middleware Hooks

Register before/after hooks to transform or observe captured requests:

```typescript
import { ProxyServer } from 'hookbox';

proxy.addHook({
  name: 'log-requests',
  phase: 'before',
  handler: (request) => {
    console.log(`Incoming: ${request.method} ${request.path}`);
    return request;
  },
});

proxy.addHook({
  name: 'add-metadata',
  phase: 'after',
  handler: (request) => {
    // Post-capture processing
    return request;
  },
});
```

## License

MIT