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

@kadi.build/tunnel-services

v1.0.6

Published

Unified tunnel management for exposing local ports via ngrok, serveo, localtunnel, KĀDI, and more

Readme

@kadi.build/tunnel-services

Unified tunnel service manager for exposing local ports to the internet. Supports 6 tunnel providers with automatic failover, KĀDI as the default primary service.

Features

  • 🚇 6 Tunnel Providers — KĀDI, Serveo, ngrok, LocalTunnel, Pinggy, localhost.run
  • 🔄 Automatic Fallback — Seamlessly switches to the next provider on failure
  • 🎯 KĀDI-First — KĀDI is the default primary tunnel service
  • 📡 Event-Driven — Rich event system for monitoring tunnel lifecycle
  • 🔧 Diagnostic Tools — Built-in diagnostics for troubleshooting
  • 🧩 Pluggable — Register custom tunnel services easily
  • 📦 Zero Config — Works out of the box with sensible defaults

Installation

npm install @kadi.build/tunnel-services

Optional Dependencies

Install based on which providers you want to use:

# For ngrok support
npm install @ngrok/ngrok

# For localtunnel support  
npm install localtunnel

# SSH-based providers (serveo, pinggy, localhost.run, kadi) 
# require `ssh` on your PATH — no extra packages needed

Quick Start

One-Liner

import { expose } from '@kadi.build/tunnel-services';

// Expose port 3000 to the internet
const url = await expose(3000);
console.log(`Public URL: ${url}`);

With Tunnel Management

import { createTunnel } from '@kadi.build/tunnel-services';

const tunnel = await createTunnel(3000, {
  service: 'kadi',         // or 'serveo', 'ngrok', 'localtunnel', 'pinggy', 'localhost.run'
  subdomain: 'my-app',     // optional
  autoFallback: true        // try next provider on failure
});

console.log(`Public URL: ${tunnel.publicUrl}`);
console.log(`Local Port: ${tunnel.localPort}`);
console.log(`Service:    ${tunnel.service}`);

// When done
await tunnel.close();

Full Control with TunnelManager

import { TunnelManager } from '@kadi.build/tunnel-services';

const manager = new TunnelManager({
  primaryService: 'kadi',
  fallbackServices: ['serveo', 'ngrok', 'localtunnel', 'pinggy'],
  autoFallback: true,
  maxConcurrentTunnels: 10,
  connectionTimeout: 30000
});

await manager.initialize();

// Listen for events
manager.on('tunnelCreated', ({ id, publicUrl, service }) => {
  console.log(`Tunnel ${id} created on ${service}: ${publicUrl}`);
});

manager.on('tunnelDestroyed', ({ id }) => {
  console.log(`Tunnel ${id} closed`);
});

manager.on('serviceFailed', ({ service, error }) => {
  console.warn(`${service} failed: ${error.message}, trying next...`);
});

// Create tunnels
const tunnel1 = await manager.createTunnel(3000);
const tunnel2 = await manager.createTunnel(8080, { service: 'ngrok' });

// Check status
console.log(manager.getStatus());

// Close specific tunnel
await manager.closeTunnel(tunnel1.id);

// Cleanup everything
await manager.cleanup();

API Reference

Factory Functions

expose(port, service?)Promise<string>

Returns a public URL string. Simplest way to expose a port.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | port | number | — | Local port to expose | | service | string | 'kadi' | Tunnel service to use |

createTunnel(port, options?)Promise<TunnelInfo>

Creates a tunnel and returns a TunnelInfo object with a close() method.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | port | number | — | Local port to expose | | options.service | string | 'kadi' | Preferred service | | options.subdomain | string | — | Requested subdomain | | options.autoFallback | boolean | true | Enable fallback |

TunnelManager

The main orchestrator class. Extends EventEmitter.

Constructor Options

new TunnelManager({
  primaryService: 'kadi',           // Default primary service
  fallbackServices: ['serveo', 'ngrok', 'localtunnel', 'pinggy'],
  autoFallback: true,               // Auto-fallback on failure
  maxConcurrentTunnels: 10,         // Max simultaneous tunnels
  connectionTimeout: 30000,         // Connection timeout (ms)
  ngrokAuthToken: '',               // Ngrok auth token
  kadiServer: '',                   // KĀDI tunnel server
  kadiMode: 'ssh'                   // 'ssh' or 'frpc'
})

Methods

| Method | Returns | Description | |--------|---------|-------------| | initialize() | Promise<void> | Discover and load services | | createTunnel(port, options?) | Promise<TunnelInfo> | Create a new tunnel | | closeTunnel(id) | Promise<void> | Close specific tunnel | | closeAllTunnels() | Promise<void> | Close all active tunnels | | getStatus() | object | Manager status + active tunnels | | getAvailableServices() | string[] | List discovered services | | testService(name) | Promise<object> | Test if a service is available | | runDiagnostics() | Promise<object> | Run diagnostics on all services | | cleanup() | Promise<void> | Full shutdown |

Events

| Event | Payload | Description | |-------|---------|-------------| | tunnelCreated | TunnelInfo | Tunnel successfully created | | tunnelDestroyed | { id } | Tunnel closed | | serviceFailed | { service, error } | Service failed, falling back | | error | Error | Unrecoverable error |

TunnelInfo Object

{
  id: 'tunnel-abc123',          // Unique tunnel ID
  publicUrl: 'https://...',      // Public URL
  localPort: 3000,              // Local port being exposed
  service: 'kadi',              // Service that created it
  subdomain: 'my-app',         // Subdomain (if requested)
  createdAt: Date,              // Creation timestamp
  metadata: {}                  // Service-specific metadata
}

BaseTunnelService

Abstract base class for all tunnel services. Extend this to create custom providers.

import { BaseTunnelService } from '@kadi.build/tunnel-services';

class MyTunnelService extends BaseTunnelService {
  get name() { return 'my-tunnel'; }

  async connect(port, options = {}) {
    // Connect and return { url, port, ... }
  }

  async disconnect() {
    // Clean up
  }

  getStatus() {
    return { connected: this._connected, service: this.name };
  }
}

Error Classes

| Error | Fallback? | Description | |-------|-----------|-------------| | TunnelError | — | Base tunnel error | | TransientTunnelError | ✅ Yes | Temporary failure, try next service | | PermanentTunnelError | ❌ No | Permanent failure, do not fallback | | CriticalTunnelError | ❌ No | Critical system-level failure | | ConfigurationError | ❌ No | Invalid configuration | | ServiceUnavailableError | ✅ Yes | Service not available/installed | | ConnectionTimeoutError | ✅ Yes | Connection timed out | | SSHUnavailableError | ✅ Yes | SSH binary not found | | AuthenticationFailedError | ❌ No | Authentication failed |

Tunnel Providers

KĀDI (default)

SSH or frpc-based tunnel to KĀDI infrastructure. Supports two modes:

  • SSH (zero-dependency) — uses ssh -R through the frps SSH gateway
  • frpc (enhanced) — uses the frpc binary with auto-reconnect

In auto mode (the default), frpc is used when the binary is detected on $PATH, otherwise SSH is used as a fallback.

Since v0.4.0, frpc mode connects through a WSS gateway on port 443 by default, which works reliably on enterprise/campus networks that block non-standard ports.

import { KadiTunnelService } from '@kadi.build/tunnel-services';

const kadi = new KadiTunnelService({
  frp: {
    serverAddr: 'broker.kadi.build',
    serverPort: 7000,
    sshPort: 2200,
    token: '...',
    tunnelDomain: 'tunnel.kadi.build',
    mode: 'auto',                                    // 'auto' | 'ssh' | 'frpc'
    transport: 'wss',                                // 'wss' (default) | 'tcp'
    wssControlHost: 'tunnel-control.kadi.build'      // WSS gateway hostname
  },
  agentId: 'my-agent'
});

| Field | Required | Default | Description | |-------|----------|---------|-------------| | serverAddr | Yes | — | frps server address | | serverPort | No | 7000 | frps bind port (TCP fallback) | | sshPort | No | 2200 | frps SSH gateway port | | token | Yes | — | Authentication token | | tunnelDomain | Yes | — | Base domain for tunnel URLs | | mode | No | 'auto' | 'auto', 'ssh', or 'frpc' | | transport | No | 'wss' | frpc transport: 'wss' (port 443) or 'tcp' (direct) | | wssControlHost | If transport='wss' | '' | WSS gateway hostname |

Environment Variables: KADI_TUNNEL_SERVER, KADI_TUNNEL_PORT

Serveo

Free SSH-based tunneling via serveo.net. No account required.

await manager.createTunnel(3000, { service: 'serveo' });

Requires: ssh on PATH

ngrok

Industry-standard tunnel service. Requires auth token for extended use.

const manager = new TunnelManager({
  primaryService: 'ngrok',
  ngrokAuthToken: process.env.NGROK_AUTH_TOKEN
});

Requires: @ngrok/ngrok or legacy ngrok package
Environment Variables: NGROK_AUTH_TOKEN

LocalTunnel

Open-source tunneling via localtunnel.me.

await manager.createTunnel(3000, { service: 'localtunnel' });

Requires: localtunnel npm package

Pinggy

SSH-based tunneling via pinggy.io.

await manager.createTunnel(3000, { service: 'pinggy' });

Requires: ssh on PATH

localhost.run

SSH-based tunneling via localhost.run. No account required.

await manager.createTunnel(3000, { service: 'localhost.run' });

Requires: ssh on PATH

Fallback Order

When autoFallback: true (default), the manager tries services in this order:

  1. KĀDI (primary)
  2. Serveo
  3. ngrok
  4. LocalTunnel
  5. Pinggy

If a service throws a TransientTunnelError or ServiceUnavailableError, the next service in the chain is attempted. PermanentTunnelError stops the fallback chain.

Testing

# Run all tests
npm test

# Run specific test suite
npm run test:manager
npm run test:kadi
npm run test:ngrok
npm run test:serveo

# Integration tests (require credentials/tools)
NGROK_AUTH_TOKEN=xxx npm run test:ngrok
KADI_TUNNEL_SERVER=tunnel.kadi.build npm run test:kadi

Environment Variables

| Variable | Used By | Description | |----------|---------|-------------| | NGROK_AUTH_TOKEN | ngrok | Authentication token | | KADI_TUNNEL_SERVER | KĀDI | Tunnel server hostname | | KADI_TUNNEL_PORT | KĀDI | Tunnel server port | | DEBUG | all | Debug logging (e.g., kadi:tunnel:*) |

Requirements

  • Node.js >= 18.0.0
  • SSH on PATH (for serveo, pinggy, localhost.run, kadi)
  • Optional: @ngrok/ngrok, localtunnel npm packages

License

MIT © KĀDI