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

@uued/dsh-ssh-tunnel

v0.2.0

Published

Standalone SSH-tunnel remote connections for dsh: manage loopback-only `ssh -N -T -L 127.0.0.1:<LP>:127.0.0.1:<RP>` tunnels to remote dsh instances from the Web UI (sidebar capsule + Settings section), test reachability, and open each remote instance in a

Readme

dsh-ssh-tunnel

Host-plane Cordis plugin for dsh: connect to remote dsh instances over loopback-only SSH tunnels — extracted from @uued/dsh-desktop's ssh connection mode, minus the native desktop shell. Works in the normal browser workflow: the sidebar "SSH 隧道" capsule and the Settings section manage connections; clicking 连接 opens the tunnel and the remote dsh UI in a new browser tab, so the local instance (and its disconnect control) stays alive.

Install (official, one command — no compilation, no manual wiring)

The package is an npm bundle (dsh.bundle.patch ships cordis.patch.yml), so a single dsh plugin call installs the plugin AND activates it:

# from a registry build (published):
dsh plugin --profile web add @uued/dsh-ssh-tunnel

# or from a local source checkout:
dsh plugin --profile web add file:/data/dsh_plugins/dsh-ssh-tunnel

What happens automatically:

  1. pnpm installs @uued/dsh-ssh-tunnel.
  2. dsh plugin reconcile sees dsh.bundle.patch → appends the package to the profile's dsh.profile.bundles.
  3. At boot, the bundle's cordis.patch.yml inserts the plugin row automatically. Do NOT also insert it into the profile's own cordis.patch.yml — that would mount it twice.

Scope placeholder: published names use the @uued scope. Swap it for your real npm scope in: package.json (name), cordis.patch.yml (insert name), and lib/client.js (module id) before first publish.

How it works

 dsh web process (this plugin)                    your browser
 ┌────────────────────────────────────┐   fetch   ┌─────────────────────────────┐
 │ TunnelManager (Map id → ssh child) │◀─────────▶│ sidebar capsule + Settings │
 │  allocLoopbackPort() → 127.0.0.1   │  POST     │ "连接" → new tab opens     │
 │  ssh -N -T -L <LP>:<RP> (loopback) │  connect  │ http://127.0.0.1:<LP>      │
 │  probeTunnelReady() → HTTP GET     │──────────▶│ (the REMOTE dsh instance)  │
 │  teardown() on plugin dispose      │           └─────────────────────────────┘
 └────────────────────────────────────┘
  • Loopback-only, always: every tunnel is ssh -N -T -o ExitOnForwardFailure=yes -o ConnectTimeout=10 -L 127.0.0.1:<LP>:127.0.0.1:<RP> — never bound to 0.0.0.0. The tunnel is a privileged path into a remote instance and must never be exposed on the LAN.
  • Multi-tunnel: every connected id keeps its own ssh child until disconnected; connecting more hosts does not kill earlier tunnels (each still has its own browser tab).
  • Readiness probe: the plugin HTTP-GETs http://127.0.0.1:<LP>/ (or …/?token=… when a token is set) every 400 ms until the remote dsh answers, or the ssh process exits, or timeoutMs (default 20000) elapses.
  • Lifecycle: tunnels are spawned through the harness subprocess service (detached, process-grouped tree); teardown() terminates every ssh child on plugin disposal or dsh exit — no orphans.
  • Key-based auth only: ssh -N -T with stdin: "ignore" makes interactive password prompts impossible by design — use key (a path, ~ expands) or a running ssh-agent. extraArgs can carry any additional -o options. The per-connection token is a dsh Web UI access token (appended to the opened URL + probe), never an ssh credential.
  • New-tab opening: the client reserves the new tab synchronously in the click handler (so popup blockers allow it), then navigates it once the tunnel is up. If the popup is blocked, the UI shows the tunnel URL as a link instead — nothing is lost.

Troubleshooting "ssh tunnel to did not become ready"

The readiness probe (HTTP GET on the loopback endpoint, timeoutMs max) failed, so the tunnel was torn down and the connection/test errored. Since 0.1.1 the error message carries the real cause (ssh stderr tail + exit code), e.g.:

  • … (ssh exited: code=255) + Permission denied (publickey) → the private key is not authorized on the remote host, or the connection's key field points at the wrong file. Two classic causes:
    • key points at the public key (~/.ssh/id_ed25519.pub) — ssh -i needs the private key file; since 0.1.2 the plugin rejects a .pub path with a clear error at save/connect time.
    • the private key file is world-readable (Permissions 0644 … too open) — chmod 600 ~/.ssh/id_ed25519.
  • Host key verification failed → the remote host is not in known_hosts and non-interactive ssh cannot confirm its fingerprint: run a plain ssh <user>@<host> once and accept the host key (then retry).
  • connect failed: Connection refused on the far side (exit with ExitOnForwardFailure) → the remote dsh is not listening on remotePort (default 3080) — check ss -ltn on the target.
  • Connection timed out / No route to host after ~10 s → host unreachable / port 22 filtered from this machine.

Manual reproduction (run on the machine whose dsh web UI you clicked in): ssh -N -T -o ExitOnForwardFailure=yes -L 127.0.0.1:39998:127.0.0.1:3080 <user>@<host> then in another terminal curl -v http://127.0.0.1:39998/ — 200 means the tunnel works and only the probe was too slow; Connection refused means the remote port is wrong.

Config (dsh-ssh-tunnel: namespace in settings.yaml)

dsh-ssh-tunnel:
  timeoutMs: 20000          # ms to wait for a tunnel to answer the probe
  connections:
    - id: home-server       # arbitrary unique id (also an SSH connection to a
      host: my.box          # REMOTE dsh, e.g. a headless server)
      port: 22
      user: me
      key: ~/.ssh/id_ed25519
      remotePort: 3080      # the remote dsh `--port` (the <RP>)
      localPort: 0          # <LP> the LOCAL loopback bind port; 0/absent = auto-allocate a free one
                            #   (set a fixed port e.g. 3081 to keep `http://127.0.0.1:3081` stable)
      token: ""             # the remote dsh WEB UI access token from `dsh web`'s
                            #   `http://127.0.0.1:<port>/?token=…` line; appended as `?token=…`
                            #   to the opened URL and the readiness probe (optional)
      # extraArgs: ["-o", "ServerAliveInterval=30"]

Browser-plane UI

  • Sidebar footer capsule "SSH 隧道" (order 21): shows how many tunnels are up; the popover lists every connection with 连接/断开, per-row status, and a 全部断开 footer. Refreshed on open + every 5 s while open (an ssh child can die on its own).
  • Settings section "SSH 隧道连接" (order 90): CRUD for connections (id/host/user/key/port/remotePort/localPort/token/extraArgs) + 测试/连接/断开 per row. localPort (本地映射端口) default 0 = auto-allocate a free loopback port (set a fixed port to keep http://127.0.0.1:<LP> stable; an occupied port errors instead of silently moving). token (访问令牌) is the remote dsh web UI access token, appended to the opened URL as ?token=…. Saving persists first; it never auto-connects.

The browser half talks to the host ONLY over same-origin HTTP routes (registered once webServer is bound; no CORS — every call is same-origin, and the new tab is served by the remote instance itself):

GET  /plugins/dsh-ssh-tunnel/state      → { state, connections:[{id,host,user,key,port,remotePort,configuredLocalPort,token,extraArgs,connected,localPort,url}] }
POST /plugins/dsh-ssh-tunnel/connect    → { id } → { ok, connected, id, localPort, url }  (localPort = actual bind; url carries ?token=… when set)
POST /plugins/dsh-ssh-tunnel/disconnect → { id? } (no id → disconnect all)
POST /plugins/dsh-ssh-tunnel/test       → { id } → { ok, url?, error?, durationMs }  (probe-only, never leaves a tunnel)
GET|POST /plugins/dsh-ssh-tunnel/config → { connections:[...] } (persisted via the settings namespace)

localPort in the state/connect routes is the actual loopback bind port; configuredLocalPort is the requested value (0 = auto-allocate).

The token is the dsh Web UI access token — it is appended to the URL the browser opens and to the HTTP readiness probe. It is not an ssh-auth credential: ssh -N -T stays key-based only, so the token never reaches the ssh command line.

Testing

Unit tests (pure tunnel machinery + TunnelManager lifecycle, no real ssh):

node --test dsh-ssh-tunnel/test/unit.mjs        # 35 checks

Manual end-to-end with a LOCAL sshd (see test/e2e-patch.yml for the full recipe): run dsh web with the patch, POST connect for a 127.0.0.1 sshd connection (passwordless key), verify http://127.0.0.1:<LP>/ serves the UI through the tunnel, disconnect, then confirm no ssh leftover:

pgrep -af 'ssh .*127.0.0.1:'    # → nothing

Relationship to @uued/dsh-desktop

The ssh-tunnel machinery in this package started as a copy of dsh-desktop's ssh connection mode (same sshTunnelSpec/probeTunnelReady semantics, same loopback-only guarantee). The two packages are independent: dsh-desktop keeps its own ssh support (inside its native-shell world); this plugin targets the plain browser workflow. Neither imports from the other.

Wiring (this workspace)

The workspace web profile uses the official bundle model:

  • .dsh-home/profiles/web/package.json: dependency "@uued/dsh-ssh-tunnel": "link:/home/hp400g5/plugin_dev/dsh-ssh-tunnel" and dsh.profile.bundles includes @uued/dsh-ssh-tunnel (a bundle-declaring dep; dsh plugin adds it automatically).
  • node_modules/@uued/dsh-ssh-tunnel → symlink to this directory (mirrored from the existing @uued/dsh-desktop link).
  • No manual insert in the profile's cordis.patch.yml — the bundle's own cordis.patch.yml mounts the row.