<!-- deno-fmt-ignore-file -->

localtunnel
===========

[![JSR][JSR badge]][JSR]
[![npm][npm badge]][npm]

This package is a simple wrapper around the local tunneling services.
Currently it supports [serveo.net] and [Pinggy].
The module is designed to be simple to use and to provide a consistent interface
to those services.

[JSR]: https://jsr.io/@hongminhee/localtunnel
[JSR badge]: https://jsr.io/badges/@hongminhee/localtunnel
[npm]: https://www.npmjs.com/package/@hongminhee/localtunnel
[npm badge]: https://img.shields.io/npm/v/@hongminhee/localtunnel?logo=npm
[serveo.net]: https://serveo.net/
[Pinggy]: https://pinggy.io/


Installation
------------

It is available on [JSR] and [npm]:

~~~~ console
deno add --jsr @hongminhee/localtunnel  # Deno
npm  add       @hongminhee/localtunnel  # npm
pnpm add       @hongminhee/localtunnel  # pnpm
yarn add       @hongminhee/localtunnel  # Yarn
bun  add       @hongminhee/localtunnel  # Bun
~~~~


Usage
-----

Invoke the [`openTunnel()`] function to open a tunnel to a local port.
The function returns a promise that resolves to a [`Tunnel`] object.
The `Tunnel` object has a [`url`] property that contains the `URL` of the tunnel.
The `Tunnel` object also has a [`close()`] method that closes the tunnel:

~~~~ typescript
const tunnel = await openTunnel({ port: 8000 });
console.log(tunnel.url.href);
alert("Press Enter to close the tunnel.");
await tunnel.close();
~~~~

By default, `openTunnel()` chooses from [`SERVICES`].  To use a different set
of services, pass a name-to-[`Service`] registry through the `services` option.
The registry replaces the default services for the call and any retries:

~~~~ typescript
import { openTunnel, type Service, SERVICES } from "@hongminhee/localtunnel";

const services = {
  "pinggy.io": SERVICES["pinggy.io"],
  custom: {
    host: "tunnel.example.com:2222",
    port: 80,
    knownHosts: {
      "[tunnel.example.com]:2222": ["ssh-ed25519 AAAA..."],
    },
    urlPattern: /https:\/\/[^\s]+\.example\.com/,
  },
} satisfies Readonly<Record<string, Service>>;

const tunnel = await openTunnel({
  port: 8000,
  services,
  service: "custom",
});
~~~~

The `service` and `exclude` options accept only keys from the selected
registry.  If `service` is omitted, `openTunnel()` chooses a service at random
and retries with the remaining services when a connection fails or does not
provide a URL within 10 seconds.  The startup deadline can be changed with the
`startupTimeout` option.

Set a service's `knownHosts` field to pin its SSH host keys.  The object maps
OpenSSH host patterns to arrays of public keys; use the `[hostname]:port` form
for a nonstandard port and list multiple keys during key rotation.  Each key
must be a single public-key line without the host pattern.  When `knownHosts`
is present, localtunnel uses only those keys from a private temporary
`known_hosts` file, enables strict host-key checking, and removes the file when
startup fails or the tunnel closes.  Omitting `knownHosts` retains the legacy
non-strict behavior.  The built-in services in [`SERVICES`] have pinned keys.

For more information, see the [API documentation][JSR].

[`openTunnel()`]: https://jsr.io/@hongminhee/localtunnel/doc/~/openTunnel
[`Service`]: https://jsr.io/@hongminhee/localtunnel/doc/~/Service
[`SERVICES`]: https://jsr.io/@hongminhee/localtunnel/doc/~/SERVICES
[`Tunnel`]: https://jsr.io/@hongminhee/localtunnel/doc/~/Tunnel
[`url`]: https://jsr.io/@hongminhee/localtunnel/doc/~/Tunnel.url
[`close()`]: https://jsr.io/@hongminhee/localtunnel/doc/~/Tunnel.close


Changelog
---------

### Version 0.5.0

Released on July 14, 2026.

 -  Added the `knownHosts` field to `Service`.  Configured services now verify
    SSH host keys strictly using a private temporary `known_hosts` file that is
    removed after startup failure or tunnel shutdown.
 -  Pinned the current SSH host keys for the built-in Serveo and Pinggy
    services.

### Version 0.4.0

Released on July 13, 2026.

 -  Added the `services` option to `TunnelOptions` and the
    `CustomTunnelOptions` type for callers that provide a custom registry.  A
    custom registry replaces `SERVICES` for selection and retries.
 -  Exported the `Service` interface and `SERVICES` constant from the package
    entry point.
 -  The `service` and `exclude` options now accept service names from the
    selected registry instead of `Service` objects.
 -  Tunnel startup now times out after 10 seconds and falls back to another
    service when SSH fails, exits, or does not provide a URL.
 -  Updated the Serveo URL pattern for its current `serveousercontent.com`
    domains.
 -  Removed the `"localhost.run"` service from the default services registry
    as it is no longer functional.

### Version 0.3.1

Released on July 13, 2026.

 -  Updated the host domain of `"pinggy.io"` service from `a.pinggy.io` to
    `free.pinggy.io` and updated its URL pattern.

### Version 0.3.0

Released on August 31, 2025.

 -  Added support for Node.js and Bun.
 -  The package is now published on [npm] as well as [JSR].
 -  Every field in `Service`, `Tunnel`, and `TunnelOptions` is now `readonly`.
 -  Added `extraOptions` and `extraArgs` fields to the `Service` interface.
 -  Added `"pinggy.io"` to the `ServiceName` type.  [[#1]]

[#1]: https://github.com/dahlia/localtunnel/issues/1

### Version 0.2.0

Released on April 29, 2024.

 -  Added `exclude` option to `TunnelOptions` interface.
 -  Now `openTunnel()` automatically retries with another service if the first
    service fails.

### Version 0.1.1

Released on April 28, 2024.

 -  Fixed a bug in `openTunnel()` that breaks standard input on Windows.
 -  Improved error handling in `openTunnel()`.
 -  Added log messages using [LogTape].  The log category is `["localtunnel"]`.

[LogTape]: https://github.com/dahlia/logtape

### Version 0.1.0

Initial release.  Released on April 26, 2024.
