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

secure-tunnel

v1.0.6

Published

Defines credentials and creates a secure tunnel between a server and multiple clients. Communication channel agnostic.

Downloads

598

Readme

secure-tunnel

Defines credentials and creates a secure tunnel between a server and multiple clients. Communication channel agnostic.

Usage

Before a connection can be created between a client and server, the keys must first be created.

import { createKeyPair } from 'secure-tunnel'

const { id, clientKey, serverKey } = await createKeyPair({ id: 'MyId' })
// id = 'MyId'
// clientKey = 'MyIdaBcDeFghIjK1mN0P9RsTuVwXyZ'
// serverKey = 'l234567B9olll2l3l4l5l6l7lB'

The generated keys are cryptographically random strings, and will only contain alphanumeric values (unless the id contains something else) The server key is always 26 characters long. The client key's length is 26 characters plus the length of the id, and starts with the public id. This should be called once to create the initial keys, and the client key should be sent and saved securely; this should not be called on every connection.

To create a server, call the createServer function. It may be implemented similarly to the following:

import { createServer, createKeyPair } from 'secure-tunnel'

const { id: connectionId, clientKey, serverKey } = await createKeyPair({ id: 'MyId' })

// ...after the client persists the clientKey... //

const { onSocketOpen, onSocketMessage, onSocketClose } = createServer({
  onConnect ({ id, send, close, onMessage, onClose }) {
    console.log('connected with id ', id)
    onMessage(message => {
      console.log('Server got a message: ', `${message}`)
    })
    send('Sent message from server')
    onClose (() => {
      console.log('closing!')
    })
  },
  async getCredentials (id) {
    if (connectionId === id) {
      return serverKey
    }
    throw 'Unknown id'
  }
})

// socketApi = { send, close }
// onSocketOpen(socketApi)
// onSocketMessage(socketApi, message)
// onSocketClose(socketApi)

createServer returns methods to invoke when something akin to a websocket is opened, sends a message, or is closed. See e2e-bun or e2e-node for an example of how that might be implemented.

getCredentials is called when a client connects via websockets and sends its id as part of the auth handshake. This function should return the serverKey that was originally created via the createKeyPair function. This key might be saved in a local file (for cases where there is only one server/client connection), or multiple id/server key mappings may be saved in a database. It is up to you.

onConnect is called after the client and server connects and the connection is authenticated. The function's payload includes the id used to connect, a method to send (which calls the passed in socket api's send function), a callback function to handle messages, and a callback function to handle the socket being closed. Multiple clients can connect with the same id.

The client is created and started via the createClient function. Using ws as an example:

import { createClient, createKeyPair } from 'secure-tunnel'

const { id: connectionId, clientKey, serverKey } = await createKeyPair({ id: 'MyId' })

// ...after the client persists the clientKey... //

const client = createClient({
  clientKey,
  onConnect: ({ send, close, onMessage, onClose }) => {
    console.log('connected!')
    onMessage((message) => {
      console.log('Client got a message: ', '' + message)
    })
  },
  initializeSocket () {
    return {
      send () {},
      close () {},
      onOpen (callback) {},
      onMessage (callback) {},
      onClose (callback) {}
    }
  }
})

clientKey is the key created via the createKeyPair function that was sent from the server. This should be saved as a local file that isn't tracked in source control.

initializeSocket is called immediately as well as whenever a connection needs to be reestablished. It should return an object that contains the above functions, but the exact implementation is up to you. See e2e-bun or e2e-node for an example of how that might be implemented.

onConnect is called after the client and server connects and the connection is authenticated. The function's payload includes a method to send and close (which calls the respective functions defined in initializeSocket), a callback function to handle messages, and a callback function to handle the socket being closed.

Usage with ws

secure-tunnel does not depend on the ws library to run, but the library can be used to handle connecting the client and the server.

On the client side, if createClient returns a "ws-like" websocket object (or an object that specifies on and removeListener functions but no onOpen, onMessage, onError, or onClose functions), secure-tunnel will automatically convert the ws into an object that can be used internally.

createClient({
  clientKey,
  onConnect: ({ send, close, onMessage, onClose }) => {
    ...
  },
  initializeSocket: () => new WebSocket(`wss://my-remote-server/connect`)
})

On the server side, if wss is specified as the parameter in the createServer function, listeners adhering to the websocket server object created by new WebSocketServer will be used.

const wss = new WebSocketServer({ port: 443 })
createServer({
  onConnect ({ id, send, close, onMessage, onClose }) {
    // ...
  },
  async getCredentials (_id) {
    // ...
  },
  wss
})

See tests/e2e-node.js for an example of how to utilize these functions.

Usage with Bun

The server-end of secure-tunnel can use Bun's web server. The object returned from createServer includes a createBunWebsocketApi function that converts the api into a websocket object that Bun.serve can utilize:

const peerTunnel = createServer({
  onConnect ({ id, send, close, onMessage, onClose }) {
    // ...
  },
  async getCredentials (_id) {
    // ...
  }
})

const server = Bun.serve({
  // ...
  websocket: peerTunnel.createBunWebsocketApi()
})

See tests/e2e-bun.js for an example of how to utilize this function.

How it works

Persisted asymmetrical keys (strings) are created beforehand for both the client and server. The client's key contains an id, an 18 character randomly generated password (≈1.8E32 possible passwords), and a hash of the server's password. The server's key contains an 8 character randomly generated password (≈2.2E14 possible passwords), and a hash of the client password. Both keys consist of only alphanumeric characters, assuming the id is also alphanumeric.

How the connection is esablished is dependent on the implementation, but the code is built around the assumption that websockets are used. Once the connection between the client and the server is established, the server will start by sending a public 512-byte RSA key to the client. The RSA key pair is generated via Node's createKeyPair:

createKeyPair('rsa', {
  modulusLength: (512 + 42 + 15) * 8, // 4552
  publicKeyEncoding: {
    type: 'spki',
    format: 'der'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'der'
  }
}

Note: Due to differences in the internal implementation, Node and Bun will generate RSA keys with slightly different lengths. While somewhat unexpected, this does not affect the ability for Node and Bun clients/servers to connect.

The client's very first expected message will be the public RSA key. It uses this RSA key to encrypt the following:

  • The length of the public id in bytes
  • The public id
  • Client password
  • The current time, in seconds
  • A 30-byte nonce

The client also creates its own RSA key pair using the same algorithm above, and sends its public RSA key as well as the encrypted payload above back to the server.

The server decrypts the encrypted message sent from the client. It calls the overridden asynchronous function getCredentials with the client-specified id to get the server key. Assuming a key is returned, the server then ensures the client password matches the server's hashed client password. It also checks to make sure less than 10 seconds have elapsed since the message was sent. If everything is good, the server then sends the following, encrypted by the public RSA key the client sent:

  • Server password
  • 48 byte AES key
  • The current time, in seconds
  • The nonce sent from the client

The next message the client expects is the server's payload above encrypted with the public RSA key the client created. The client decrypts the message, checks to make sure the time is within 10 seconds of the original handshake request, the nonce the client sent is the same, and that the sent server password matches the client's hashed server password. If everything is good, the client saves the AES key the server sent.

Once the client and server are authenticated, all future messages sent between them will be encrypted with the AES key.

Development

This is a Bun project.

To install dependencies:

bun install

To run unit tests (can take awhile):

bun run test

To build:

bun run build

Some proof-of-concept E2E tests also exist. These are mostly for reference but can also be run:

bun run test-bun
bun run test-bun-rest
bun run test-node