@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-servicesOptional 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 neededQuick 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 -Rthrough the frps SSH gateway - frpc (enhanced) — uses the
frpcbinary 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:
- KĀDI (primary)
- Serveo
- ngrok
- LocalTunnel
- 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:kadiEnvironment 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,localtunnelnpm packages
License
MIT © KĀDI
