@dbx-tools/cli-tunnel
v0.6.212
Published
Public Portr and FRP tunnel commands protected by the dbx-tools authentication gate
Readme
@dbx-tools/cli-tunnel
Wrap any command in a public Portr,
FRP, or combined tunnel fronted by
@dbx-tools/auth-gate email OTP and passkeys.
Run dbx tunnel -- <command> when a local or self-hosted process needs a public
URL that only approved email addresses can reach. The wrapper claims the public
port, starts your command on a private loopback port, and reverse-proxies between
them so the gate sits in front of traffic the command itself never has to know
about.
Key features:
- Portr, FRP, or both tunnel clients + a Better Auth passwordless gate around a command the wrapper does not have to modify, import, or even be written in the same language as.
- A reverse proxy that answers the login routes itself and forwards only verified traffic to the wrapped process.
- Flag → environment →
databricks.ymlresolution for every gate and tunnel setting, delegated to@dbx-tools/tunnelso the CLI cannot drift from the in-process plugin. - The same server-less
appkit.createApplifecycle as plugin mode. It can register nativelakebase()withoutserver(), or use local SQLite. statusto print exactly what would happen without starting anything.- Two-way process supervision: a crashed child takes the tunnel down instead of leaving portr serving a dead port.
- Lazy loading, so
--insecure,status, andinstallnever load AppKit, the Databricks SDK, or the SMTP stack.
Why Not The AppKit Plugin?
Use the in-process path when you can. An app that boots through
@dbx-tools/appkit's createApp should register
tunnelInterceptor() plus the authGate plugin from
@dbx-tools/tunnel: one process, no proxy hop, no
duplicated header handling.
Use this wrapper for the case that path cannot cover:
- a project that does not call
appkit.createApp, so there is no plugin lifecycle to register a gate in; - a process that is not a Node/AppKit server at all - a Python service, a static file server, a third-party binary;
- a command you want gated without editing its source.
The gating DECISION is shared either way. The proxy calls @dbx-tools/tunnel's
gate.gateRequest - the same function the Express middleware uses - so which
requests are gated and which headers are stripped has exactly one
implementation.
Run A Tunnel
dbx tunnel --allow databricks.com -- bun src/server.tsThis package ships no bin. It contributes the tunnel command group to the
single dbx CLI in @dbx-tools/cli, which is what you install:
npm install --global @dbx-tools/cli
dbx tunnel --helpdbx imports this package lazily, so dbx dev pays for none of it.
The command after -- is spawned with DATABRICKS_APP_PORT, PORT, and
HOST=127.0.0.1 pointing at a private port, so a server that honors those
variables needs no changes. run is both the default action and a named
subcommand, so dbx tunnel -- cmd and dbx tunnel run -- cmd are equivalent.
Check What Would Happen
dbx tunnel status --allow databricks.comThe most common failure is a tunnel that silently does nothing because its
credentials or public domain did not resolve. status prints the fully resolved
ports, gate config, and client configs as JSON without starting a process.
dbx tunnel installinstall defaults to Portr. Pass frp or both to preinstall those clients:
dbx tunnel install frp
dbx tunnel install bothCommands And Flags
dbx tunnel [options] -- <command...> # wrap a command (default)
dbx tunnel run [options] -- <command...>
dbx tunnel status [options]
dbx tunnel install [portr|frp|both]Every flag below is accepted on the root command and on run / status.
Omitted values fall back to the environment, a .env file, then
databricks.yml, through @dbx-tools/core's config.
| Flag | Meaning |
| ----------------------------- | ---------------------------------------------------------- |
| --transport <mode> | portr (default), frp, or both |
| --public-domain <host> | Portr public domain (<subdomain>.<server>) |
| --subdomain <name> | portr subdomain, else derived from the public domain |
| --frp-public-domain <host> | FRP public HTTP domain |
| --frp-server <host> | frps control host, else the FRP public domain |
| --frp-server-port <port> | frps control port (default 443) |
| --frp-protocol <protocol> | frpc transport protocol (default wss) |
| --frp-token <token> | optional frps auth token |
| --frp-proxy-name <name> | frp proxy name, else the domain's first label |
| --port <port> | public port the wrapper listens on (DATABRICKS_APP_PORT) |
| --app-port <port> | private port the wrapped app binds, else a free one |
| --allow <patterns...> | email allow-list (domain, glob, or /regex/) |
| --subject <text> | verification email subject |
| --brand-name <name> | verification email brand name |
| --message <text> | verification email message |
| --session-ttl <seconds> | session lifetime |
| --code-ttl <seconds> | one-time-code lifetime |
| --session-cutoff <when> | invalidate every session issued before this |
| --auth-storage <mode> | Better Auth database: auto, lakebase, or sqlite |
| --auth-sqlite-path <path> | local Better Auth SQLite file |
| --forward-headers <pats...> | extra x- headers tunnel traffic may forward |
| --insecure | run open, with no gate |
Leave --subject and --brand-name alone unless you have a reason: the
defaults are the conventional one-time-code wording that iOS, Gmail, Outlook,
and Android detect for autofill, and a novel subject breaks that.
--insecure serves the tunnel with no gate at all and logs a warning. It is for
local debugging, not for anything reachable.
How A Request Flows
- The wrapper binds the PUBLIC port - the one tunnel clients and Databricks Apps runtime route to - and spawns the command on a private loopback port.
- A login route (
/auth/*) is answered by the proxy itself, using the gate handlers from a server-less AppKit app that supplies the code store, signing key, and email transport. - Anything else goes through
gate.gateRequest. A verified session is proxied to the child with caller-suppliedx-headers stripped; an unverified request gets the login page or a401. - The selected client(s) publish the public port. In
bothmode, configure different Portr and FRP domains; the gate recognizes both hosts.
Modules
cli- thedbx tunnelcommander program:buildProgram(name?), which@dbx-tools/climounts.options-resolveTunnelOptions(), flag → config → default resolution.proxy-startProxy(), the gate-aware reverse proxy.app-startGateApp(), the server-less AppKit app behind the gate.
Gate behavior, portr lifecycle, and header policy live in
@dbx-tools/tunnel; email delivery in
@dbx-tools/email.
