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

wavetunnel

v0.7.6

Published

WaveTunnel client — expose a local HTTP or TCP service through your self-hosted gateway, with a built-in request-inspection dashboard

Readme

wavetunnel

Self-hosted HTTP and raw TCP reverse tunnel CLI. Connects to a WaveTunnel gateway (default https://gateway.warungkeys.com) and exposes a local service to the internet: HTTP through a random *.warungkeys.com subdomain, or any TCP byte-stream (Postgres, SSH, Redis, …) through a public gateway.warungkeys.com:<port>.

Install

npm install -g wavetunnel

Requires Node.js ≥ 20.

Quick start

wavetunnel

The first run prompts for your email, sends a 6-digit verification code, asks for the code, then asks for the local port (default 3000). After that your access token is cached in ~/.wavetunnel/config.json (chmod 600) and you skip straight to the port prompt.

Common commands

| Command | What it does | |---|---| | wavetunnel | Login if needed, prompt port, run a tunnel | | wavetunnel 8080 | Same, target http://localhost:8080 | | wavetunnel --port 8080 | Equivalent | | wavetunnel --target http://127.0.0.1:8080 | Custom target URL | | wavetunnel --tcp --port 5432 | Raw TCP tunnel to 127.0.0.1:5432 | | wavetunnel --tcp --target host:port | Raw TCP tunnel to an arbitrary host:port | | wavetunnel --basic-auth user:pass | Require HTTP Basic auth in front of the tunnel | | wavetunnel start | Run every tunnel in ~/.wavetunnel/tunnels.json | | wavetunnel start --config ./tunnels.json | Run tunnels from a specific file | | wavetunnel --no-inspect | Disable the local request-inspection dashboard | | wavetunnel --inspect-port 4050| Bind the inspection dashboard to a custom loopback port | | wavetunnel --gateway https://gateway.warungkeys.com | Override gateway | | wavetunnel --email [email protected] | Pre-fill email for the OTP prompt | | wavetunnel --token <ACCESS> | Manual mode, no interactive login | | wavetunnel --no-save-token | Don't persist token to disk | | wavetunnel login | Force a fresh OTP login | | wavetunnel logout | Revoke + clear local token | | wavetunnel whoami | Show account email + active-tunnel count |

TCP tunnels (--tcp)

By default wavetunnel exposes a local HTTP service. Add --tcp to tunnel a raw byte-stream instead — databases, SSH, Redis, anything TCP. The gateway allocates a public port and bridges every connection to your local target:

wavetunnel --tcp --port 5432            # 127.0.0.1:5432
wavetunnel --tcp --target 192.168.1.10:22   # arbitrary host:port

The banner prints the allocated public endpoint:

  Type       : TCP (raw)
  Public     : tcp://gateway.warungkeys.com:20000
  Forwarding : tcp://127.0.0.1:5432
  Connect    : point your client at gateway.warungkeys.com:20000

Connect with any TCP client, e.g. psql "host=gateway.warungkeys.com port=20000 …" or ssh -p <port> [email protected].

The public port is server-allocated (you can't choose it yet). --tcp needs the gateway to have TCP enabled — otherwise the CLI prints TCP tunnels are disabled on this gateway. Connections to the public port are unauthenticated at the transport layer (like ngrok TCP), so rely on the exposed service's own auth.

WebSocket and streaming

HTTP tunnels forward WebSocket connections and stream responses, so realtime and SSE apps work like they do on ngrok — no extra flags.

  • WebSocket — a visitor that upgrades to WebSocket on your public subdomain is bridged through to your local app's WebSocket endpoint, preserving the request path, cookies/auth headers, the negotiated subprotocol, and both text and binary frames. This covers socket.io, Vite/Next.js HMR, and other realtime transports.
  • Streaming responses — Server-Sent Events (SSE), chunked transfer, and large downloads flow incrementally instead of being buffered until the response finishes. Bytes arrive at the visitor as your local app emits them.
  • Visitor disconnect — if the visitor hangs up mid-response, the gateway tells the client to abort the in-flight local request rather than keep reading into a dead socket.

Both require a gateway running 0.6.0+. The client negotiates capabilities from the gateway's welcome message: against an older gateway it transparently falls back to the previous one-shot (buffered) behaviour. The inspection dashboard captures streamed responses too (up to the body cap, then marked truncated).

Request inspection

For HTTP tunnels, wavetunnel runs a local inspection dashboard — the equivalent of ngrok's http://localhost:4040. It shows every request and response flowing through the tunnel and lets you replay any of them against your local service:

  Public URL : https://abc12xyz.warungkeys.com
  Inspect    : http://127.0.0.1:4040

Open http://127.0.0.1:4040 in a browser. You get a live list of captured transactions (method, path, status, timing, size), a detail view with full request/response headers and bodies (JSON is pretty-printed), and a Replay button that re-issues the request to your local target — handy for iterating on a webhook handler without re-triggering the upstream sender.

Edit & Replay: alongside plain Replay, the detail view has an "Edit & Replay" panel where you can change the method, path, headers (as JSON), and body before re-sending — ngrok's "Replay with modifications". Edited replays are tagged in the list. Replays hit your local target directly and never traverse the gateway, so a --basic-auth gate does not block them.

Captures live in a bounded in-memory ring buffer (last 200 transactions); nothing is written to disk.

| Control | Effect | |---|---| | wavetunnel --no-inspect | Disable the dashboard for this run | | wavetunnel --inspect-port <port> | Bind the dashboard to a different loopback port | | WAVETUNNEL_INSPECT=0 | Disable the dashboard (also false/off/no) | | WAVETUNNEL_INSPECT_PORT=<port> | Dashboard loopback port (default 4040) |

Security: captured bodies routinely contain auth tokens, cookies, and PII, so the dashboard binds to loopback only (127.0.0.1) and is never exposed on 0.0.0.0. The loopback interface is the trust boundary — there's no separate auth, by design. The dashboard is HTTP-only; raw TCP tunnels (--tcp) carry opaque byte streams with nothing to render, so it's disabled for them.

Multi-port

Open one terminal per service. Each invocation gets a unique random subdomain:

# terminal 1
wavetunnel 8080
# → https://abc12xyz.warungkeys.com

# terminal 2
wavetunnel 3000
# → https://qrs98pdg.warungkeys.com

The gateway caps active tunnels per account (AUTH_MAX_ACTIVE_TUNNELS_PER_EMAIL, default 5).

Multi-tunnel (wavetunnel start)

Instead of one terminal per service, define them once and run them all from a single process. Create ~/.wavetunnel/tunnels.json (or pass --config <path>):

{
  "gateway": "https://gateway.warungkeys.com",
  "tunnels": [
    { "name": "web", "port": 3000 },
    { "name": "api", "target": "http://127.0.0.1:8080", "inspectPort": 4041, "basicAuth": "admin:s3cret" },
    { "name": "db",  "type": "tcp", "port": 5432, "inspect": false }
  ]
}

Then:

wavetunnel start

Each tunnel gets its own reconnect loop and inspection dashboard, with logs prefixed by [name]. You log in once; the token is shared across all tunnels on that gateway. Per-entry fields:

| Field | Default | Notes | |---|---|---| | name | tunnel-N | Label used in logs | | type | http | http or tcp | | port | — | Local port (one of port/target required) | | target | — | Explicit target (http URL, or host:port for tcp) | | inspect | true | Disable the dashboard for this tunnel (http only) | | inspectPort | 4040 | Dashboard loopback port — give each tunnel a distinct one | | basicAuth | — | user:pass gate (http only) |

The active-tunnel cap still applies, so keep the list within AUTH_MAX_ACTIVE_TUNNELS_PER_EMAIL (default 5).

Access control (--basic-auth)

Put HTTP Basic auth in front of an HTTP tunnel:

wavetunnel 3000 --basic-auth admin:s3cret

Requests without valid credentials get a 401 with a WWW-Authenticate challenge and never reach your local service. Set it per-tunnel in the tunnels file, or globally via WAVETUNNEL_BASIC_AUTH=user:pass.

Two honest caveats:

  • Enforcement happens at the client edge, not the gateway edge. An unauthenticated request still travels to the gateway and down the tunnel; the client rejects it before it touches your local service. This protects the service, but the request does reach your machine. (ngrok enforces at its edge.)
  • Credentials are compared in constant time. Inspector replays bypass the gate — they're developer-initiated from loopback, like ngrok.

Manual / CI usage

# Linux/macOS
WAVETUNNEL_TOKEN=<access-token> \
WAVETUNNEL_GATEWAY=https://gateway.warungkeys.com \
  wavetunnel --port 8080

# Windows CMD
set WAVETUNNEL_TOKEN=<access-token>
set WAVETUNNEL_GATEWAY=https://gateway.warungkeys.com
wavetunnel --port 8080

# PowerShell
$env:WAVETUNNEL_TOKEN = "<access-token>"
$env:WAVETUNNEL_GATEWAY = "https://gateway.warungkeys.com"
wavetunnel --port 8080

Environment variables

| Variable | Default | Notes | |---|---|---| | WAVETUNNEL_GATEWAY | https://gateway.warungkeys.com | Gateway base URL | | WAVETUNNEL_TOKEN | (none) | Access token (skips login) | | WAVETUNNEL_TARGET | http://localhost:3000 | Local target (HTTP URL, or host:port with --tcp) | | WAVETUNNEL_REQUEST_TIMEOUT_MS| 30000 | Per-request timeout | | WAVETUNNEL_MAX_BODY_BYTES | 10485760 | Max body forwarded (10 MB) | | WAVETUNNEL_INSPECT | (enabled) | Set to 0/false/off to disable the inspection dashboard | | WAVETUNNEL_INSPECT_PORT | 4040 | Inspection dashboard loopback port | | WAVETUNNEL_BASIC_AUTH | (none) | user:pass Basic-auth gate for HTTP tunnels | | WAVETUNNEL_TUNNELS_FILE | ~/.wavetunnel/tunnels.json | Multi-tunnel config used by wavetunnel start | | WAVETUNNEL_NO_SAVE_TOKEN | (unset) | Set to 1 to skip persistence |

Local config

~/.wavetunnel/config.json (Linux/macOS) or %USERPROFILE%\.wavetunnel\config.json (Windows). Mode 0600. Stores one entry per gateway URL. Delete the file to force a re-login.

Auto-update

When installed globally (npm install -g wavetunnel), the CLI keeps itself up to date automatically. On each run it checks the npm registry for a newer version — at most once every 24h — and if one exists, runs npm install -g wavetunnel@latest and transparently restarts on the new version.

The check is best-effort and never blocks a tunnel: if the registry is unreachable or the install fails, the CLI continues on the current version.

| Control | Effect | |---|---| | wavetunnel --no-update | Skip the update check for this run | | WAVETUNNEL_UPDATE_MODE=auto | Check, install, and restart (default) | | WAVETUNNEL_UPDATE_MODE=notify | Only print a hint when an update exists; don't install | | WAVETUNNEL_UPDATE_MODE=off | Disable auto-update entirely | | WAVETUNNEL_NO_UPDATE=1 | Disable auto-update entirely (same as off) | | WAVETUNNEL_UPDATE_INTERVAL_HOURS=N | Change the check interval (default 24) | | CI env set | Auto-update is disabled in CI |

Dev checkouts (running from a cloned repo rather than a global install) are never auto-updated.

Security notes

  • Access tokens never appear in CLI output. Use wavetunnel whoami to inspect your session, not the file.
  • The gateway uses a per-tunnel session secret to authorize the WebSocket — the long-lived access token is only used for the control-plane call to /api/tunnels. The session secret never leaves disk.
  • wavetunnel logout revokes the token server-side and clears it locally.
  • If you suspect a token leak, run wavetunnel logout from any session that has it, or revoke via DELETE /api/auth/token directly.

License

MIT