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

edgeport

v1.0.3

Published

TCP Library for Cloudflare Workers

Readme

edgeport

🛜 TCP Library for Cloudflare Workers

edgeport gives Cloudflare Workers native clients for various TCP protocols, built directly on cloudflare:sockets.

Node libraries assume Node's net/tls and Node crypto, none of which exist in workerd; edgeport is written for the Workers runtime from the ground up.

Table of Contents

Why edgeport

Workers can open raw TCP sockets, but every existing SSH/mail client depends on Node APIs the runtime does not provide. edgeport is:

  • Workers-Native. One small core is the only code that imports cloudflare:sockets; every protocol builds on it. Nothing depends on Node.
  • TypeScript-first. The types are the contract and the documentation.
  • Tree-Shakeable. Import only the protocol you use (edgeport/ssh, edgeport/smtp, ...).
  • Tested against Real Servers. Every protocol is verified under workerd against Dockerized servers (OpenSSH, Dropbear, GreenMail, NATS with JetStream, Mosquitto, ActiveMQ, OpenLDAP, an FTP server, an SMPP SMSC simulator, a Kamailio SIP server, and a WebSocket echo server) - not mocks.

Features

  • SSH transport, auth, and connection layers: exec, interactive shell, subsystems.
  • SFTP v3 over SSH: read/write/list/stat/rename/remove, plus streaming.
  • SMTP send with STARTTLS / implicit TLS and AUTH PLAIN/LOGIN.
  • IMAP and POP3 read access with STARTTLS / implicit TLS.
  • WebSocket client via the platform API (for await message iteration).
  • NATS pub/sub, request-reply, and queue groups; token / user-pass / nkey / JWT (creds) auth.
  • MQTT v3.1.1 (QoS 0/1/2, keep-alive, wildcards) over raw TCP or WebSocket.
  • STOMP 1.2 messaging (send / subscribe / ack) for ActiveMQ, RabbitMQ, and friends.
  • FTP (plaintext, passive mode): upload, download, list, and directory ops.
  • LDAP / LDAPS simple bind + search with RFC 4515 filters (BER codec, StartTLS).
  • Syslog RFC 5424 over TCP/TLS with octet-counting or LF framing.
  • SMPP v3.4 client (ESME): bind, submit_sm, and inbound deliver_sm / delivery receipts.
  • SIP / RCS user agent over TCP/TLS: register, MESSAGE, OPTIONS, presence, and MSRP chat.
  • Email-to-SMS carrier-gateway addressing layered on SMTP (sendSms).
  • Utilities (edgeport/util): hex/base64 codecs, random ids, and retry-with-backoff.
  • One uniform error vocabulary: AuthError, ConnectionError, ProtocolError, TimeoutError.

SSH algorithm support

| Category | Supported | | ---------------- | --------------------------------------------------------------------------------------------------------------- | | Key Exchange | curve25519-sha256, ecdh-sha2-nistp256 | | Host / User Keys | Ed25519, ECDSA-P256, RSA-SHA2-256/512 | | Ciphers | [email protected], [email protected], [email protected], aes256-ctr, aes128-ctr | | MAC | hmac-sha2-256, hmac-sha2-512 (AEAD Ciphers carry their own) | | Auth | publickey, password, keyboard-interactive |

ChaCha20-Poly1305 is assembled from @noble/ciphers because Workers WebCrypto does not provide it.

Install

bun add edgeport
# or: npm install edgeport

edgeport targets the Workers runtime. Use it inside a Worker, not in plain Node.

Getting Started

import { exec } from 'edgeport/ssh';

export default {
	async fetch(): Promise<Response> {
		const { stdout, code } = await exec({
			hostname: 'example.com',
			username: 'deploy',
			password: env.SSH_PASSWORD,
			command: 'uptime'
		});
		return new Response(`exit ${code}\n${new TextDecoder().decode(stdout)}`);
	}
};

SSH

One-shot Exec

import { exec } from 'edgeport/ssh';

const { stdout, stderr, code } = await exec({
	hostname: 'host',
	username: 'user',
	privateKey: { pem: env.SSH_KEY }, // PKCS8 PEM, or pass a CryptoKey
	command: 'ls -la /var/log'
});

A reusable Session

import { connect } from 'edgeport/ssh';

await using ssh = await connect({ hostname: 'host', username: 'user', password: env.PW });
const a = await ssh.exec('hostname');
const b = await ssh.exec('date');
// the session closes automatically at the end of the `await using` scope

Interactive Shell (streaming)

await using ssh = await connect({ hostname: 'host', username: 'user', password: env.PW });
await using shell = await ssh.shell();

await shell.write(new TextEncoder().encode('echo hi\n'));
const reader = shell.stdout.getReader();
const { value } = await reader.read();
console.log(new TextDecoder().decode(value));

Keyboard-Interactive and Host-Key Pinning

await using ssh = await connect({
	hostname: 'host',
	username: 'user',
	onKeyboardInteractive: async (prompts) => prompts.map(() => env.OTP),
	hostKey: {
		verify: (type, key) => type === 'ssh-ed25519' /* && key matches your pinned host key */
	}
});

Executing with Sudo

sudo does not read its password from SSH auth - it prompts over the channel. The sudo helpers run sudo -S -p '' (read the password from stdin, silence sudo's own prompt) and feed it the secret for you. The sudoers policy must allow password sudo without a tty (requiretty off, the default on most modern distros).

One-shot over a fresh connection - sudoPassword defaults to password, so the SSH login password is reused as the sudo password (the common case):

import { sudoExec } from 'edgeport/ssh';

const { stdout, code } = await sudoExec({
	hostname: 'host',
	username: 'user',
	password: env.PW,
	command: 'systemctl restart myapp'
});
console.log(code, new TextDecoder().decode(stdout));

Reusing an already-open session (pass the sudo password explicitly):

import { connect, sudo } from 'edgeport/ssh';

await using ssh = await connect({ hostname: 'host', username: 'user', password: env.PW });
const { stdout } = await sudo(ssh, 'whoami', { password: env.PW });
console.log(new TextDecoder().decode(stdout)); // "root"

Forcing a Cipher

await using ssh = await connect({
	hostname: 'host',
	username: 'user',
	password: env.PW,
	algorithms: { cipher: ['[email protected]'] }
});

Encrypted (Passphrase-Protected) Keys

Encrypted PKCS#8 (BEGIN ENCRYPTED PRIVATE KEY) and OpenSSH-format keys (BEGIN OPENSSH PRIVATE KEY, the ssh-keygen default), encrypted or not, are accepted - pass the passphrase:

await using ssh = await connect({
	hostname: 'host',
	username: 'user',
	privateKey: { pem: env.SSH_KEY, passphrase: env.SSH_KEY_PASSPHRASE }
});

Key Re-Exchange (Rekey)

Long-lived sessions and large transfers rekey automatically (default every ~1 GiB, per RFC 4253 §9); server-initiated rekeys are handled transparently. Tune or force it:

await using ssh = await connect({
	hostname: 'host',
	username: 'user',
	password: env.PW,
	rekeyThresholdBytes: 256 * 1024 * 1024 // auto-rekey every 256 MiB (0 disables)
});
await ssh.rekey(); // or force one now

Port Forwarding (Tunneling)

forwardOut opens a direct-tcpip channel: the SSH server connects to a target on your behalf and pipes the bytes back, so a Worker can reach a service that isn't internet-exposed through an SSH bastion (the -L reach-through). It returns a duplex channel - stdout is inbound bytes, write() sends outbound.

import { connect } from 'edgeport/ssh';

await using ssh = await connect({
	hostname: 'bastion',
	username: 'u',
	privateKey: { pem: env.SSH_KEY }
});

// reach an internal-only Postgres that sits behind the bastion
await using tunnel = await ssh.forwardOut('10.0.0.5', 5432);
await tunnel.write(startupPacket);
for await (const chunk of tunnel.stdout) {
	// handle each inbound chunk from the tunneled service here
}

Workers cannot accept inbound connections, so the listening half of -L/-D (a local SOCKS listener) and remote forwarding (-R) are out of scope - the server-side reach-through is the valuable part and is fully supported. The server must permit TCP forwarding (AllowTcpForwarding).

SSH Command Helpers

On top of exec/execStream, a session carries ergonomic shell helpers. Each one single-quotes the paths and arguments it interpolates, so a space or $ in a path cannot break the command; the destructive rm also refuses obviously dangerous targets (/, ~, ., ..).

These helpers assume a POSIX shell, so they work against Linux, macOS, and other Unix remotes - stat and spawnDetached handle the GNU/BSD differences (GNU stat -c vs BSD stat -f; nohup rather than Linux-only setsid). For a Windows (cmd.exe/PowerShell) or other non-POSIX remote, drop down to run/exec/execStream with native commands.

import { connect } from 'edgeport/ssh';

await using ssh = await connect({ hostname: 'host', username: 'user', password: env.PW });

const host = await ssh.run('hostname'); // decoded, trimmed stdout; throws on a nonzero exit
if (await ssh.test('command -v docker')) {
	// docker is installed
}
if (await ssh.exists('/etc/hosts')) {
	// the file is there
}
// files and directories
await ssh.mkdirp('/srv/app/releases', { mode: 0o755 });
await ssh.writeTextFile('/srv/app/.env', 'PORT=8080\n');
const envFile = await ssh.readTextFile('/srv/app/.env');
await ssh.chmod('/srv/app/run.sh', 0o755);
const st = await ssh.stat('/srv/app'); // { size, mode, mtime, isDirectory, isSymlink }
await ssh.rm('/srv/app/tmp', { recursive: true, force: true });

// system introspection and background work
const usage = await ssh.df('/srv'); // [{ filesystem, sizeKb, usedKb, availKb, usePercent, mountedOn }]
const git = await ssh.which('git'); // '/usr/bin/git' | null
await ssh.spawnDetached('/srv/app/worker', { stdout: '/var/log/worker.log' });

For one-call workflows there are connect-run-close one-shots:

import { run, test, exists } from 'edgeport/ssh';

const out = await run({ hostname: 'host', username: 'user', password: env.PW, command: 'uptime' });
const ok = await exists({
	hostname: 'host',
	username: 'user',
	password: env.PW,
	path: '/etc/hosts'
});

SFTP

import { connect, getFile, putFile } from 'edgeport/sftp';

// one-shots
const bytes = await getFile({ hostname: 'h', username: 'u', password: p, path: '/etc/hostname' });
await putFile({ hostname: 'h', username: 'u', password: p, path: '/tmp/x', data: bytes });

// a session
await using sftp = await connect({ hostname: 'h', username: 'u', password: p });
await sftp.mkdir('/tmp/reports');
await sftp.writeFile('/tmp/reports/today.csv', new TextEncoder().encode('a,b,c\n'));
for (const entry of await sftp.list('/tmp/reports')) {
	console.log(entry.filename, entry.attrs.size);
}

// stream a large download
const stream = sftp.createReadStream('/var/log/big.log');

Reusing an Existing Session

Reuse an existing SSH session instead of opening a second connection:

import { connect as sshConnect } from 'edgeport/ssh';
import { connect as sftpConnect } from 'edgeport/sftp';

await using ssh = await sshConnect({ hostname: 'h', username: 'u', password: p });
await using sftp = await sftpConnect({ session: ssh });

Large File Transfers

For large files, use the streaming API to avoid buffering the entire file in memory:

await using sftp = await connect({ hostname: 'h', username: 'u', password: p });
const readStream = sftp.createReadStream('/var/log/big.log');
const writeStream = sftp.createWriteStream('/tmp/bigfile');

SFTP Convenience Helpers

Higher-level helpers built on the same request/response framing:

import { connect } from 'edgeport/sftp';

await using sftp = await connect({ hostname: 'h', username: 'u', password: p });

// presence check (a "no such file" status resolves false; other errors propagate)
if (!(await sftp.exists('/srv/app'))) {
	await sftp.ensureDir('/srv/app/releases/2026'); // recursive mkdir, one level per segment
}

// text and JSON round-trips (UTF-8)
await sftp.writeText('/srv/app/note.txt', 'deployed\n');
const note = await sftp.readText('/srv/app/note.txt');
await sftp.writeJson('/srv/app/config.json', { port: 8080 }, { space: 2 });
const config = await sftp.readJson<{ port: number }>('/srv/app/config.json');

await sftp.chmod('/srv/app/run.sh', 0o755);

// removal: one empty dir, a batch of files, or a whole tree
await sftp.rmdir('/srv/app/empty');
await sftp.removeMany(['/tmp/a.log', '/tmp/b.log'], { ignoreMissing: true });
await sftp.removeAll('/srv/app/releases/old'); // recursive, client-side, non-atomic

removeAll walks the tree from the client (one round trip per entry) and refuses an empty path or /; ensureDir tolerates segments that already exist and verifies the leaf is a directory.

SMTP

Port 25 is blocked on Workers, so edgeport sends over submission (587, STARTTLS) or implicit TLS (465).

import { send } from 'edgeport/smtp';

await send({
	hostname: 'smtp.example.com',
	port: 587, // STARTTLS (default)
	auth: { username: '[email protected]', password: env.SMTP_PW },
	from: '[email protected]',
	to: ['[email protected]'],
	subject: 'Deploy finished',
	text: 'The 14:00 deploy completed successfully.'
});

One-shot Connect + Send with HTML

import { connect } from 'edgeport/smtp';

await using smtp = await connect({
	hostname: 'smtp.example.com',
	port: 465,
	tls: 'implicit',
	auth: { username: 'u', password: env.SMTP_PW }
});
await smtp.send({ from: 'u@x', to: 'a@y', subject: 'Hi', html: '<h1>Hi</h1>', text: 'Hi' });

Cloudflare Email Service

Using the Cloudflare Email Service, your workers can now send emails on-demand without the additional cost of a SMTP provider. For example:

import { send } from 'edgeport/smtp';

await send({
	hostname: 'smtp.mx.cloudflare.net',
	tls: 'implicit', // port defaults to 465 for implicit
	auth: {
		username: 'api_token', // literal string, per Cloudflare docs
		password: env.CF_EMAIL_TOKEN, // API token w/ Email Sending: Edit
		mechanism: 'PLAIN' // LOGIN also supported
	},
	from: '[email protected]', // domain must be onboarded to Email Sending
	to: '[email protected]', // any valid email address rather than verified recipient
	subject: 'Hello',
	text: 'Sent from a Worker over edgeport SMTP.'
});

Attachments and Plaintext Relays

Pass attachments to build a multipart/mixed message; pass tls: 'off' to talk to a trusted internal relay or dev server with no TLS.

await send({
	hostname: 'relay.internal',
	tls: 'off', // plaintext (trusted network); 'starttls' (default) and 'implicit' also supported
	from: 'reports@internal',
	to: 'team@internal',
	subject: 'Daily report',
	text: 'Attached.',
	attachments: [{ filename: 'report.csv', content: csvBytes, contentType: 'text/csv' }]
});

Email-to-SMS Gateway

Many carriers accept email at a gateway that forwards it to the handset as a text message. sendSms builds that gateway address from a phone number and a carrier - a known key from CarrierGateways, or a raw gateway domain - then sends a short plaintext message over the normal SMTP path.

import { sendSms, smsAddress } from 'edgeport/smtp';

// resolve the gateway address (number@gateway-domain), normalizing the number
smsAddress('+1 (555) 123-4567', 'att'); // '[email protected]'

await sendSms({
	hostname: 'smtp.example.com',
	auth: { username: '[email protected]', password: env.SMTP_PW },
	from: '[email protected]',
	to: { number: '5551234567', carrier: 'verizon' },
	text: 'Your code is 123456'
});

Coverage is US + Canadian carriers (att, tmobile, verizon, sprint, cricket, rogers, bell, telus, ...); pass a raw gateway domain for anything not listed. Gateway domains are best-effort and drift over time, so a raw-domain override is the escape hatch. Use type: 'mms' for a carrier's picture-message gateway where it differs.

IMAP

import { connect, fetchRecent } from 'edgeport/imap';

const recent = await fetchRecent({
	hostname: 'imap.example.com',
	auth: { username: 'u', password: env.PW },
	mailbox: 'INBOX',
	count: 10
});

await using imap = await connect({
	hostname: 'imap.example.com',
	auth: { username: 'u', password: env.PW }
});
const { exists } = await imap.select('INBOX');
const uids = await imap.search({ unseen: true });
const messages = await imap.fetch(uids, { envelope: true, body: true });

One-shot Fetch Recent

Fetch the most recent N messages in one call (not streaming):

import { fetchRecent } from 'edgeport/imap';
const recent = await fetchRecent({
	hostname: 'imap.example.com',
	auth: { username: 'u', password: env.PW },
	mailbox: 'INBOX',
	count: 10
});

POP3

import { retrieveAll, connect } from 'edgeport/pop3';

const all = await retrieveAll({
	hostname: 'pop.example.com',
	auth: { username: 'u', password: env.PW }
});

await using pop = await connect({
	hostname: 'pop.example.com',
	auth: { username: 'u', password: env.PW }
});
const { count } = await pop.stat();
const first = await pop.retrieve(1);

One-shot Retrieve All

Retrieve all messages in one call (not streaming):

import { retrieveAll } from 'edgeport/pop3';
const all = await retrieveAll({
	hostname: 'pop.example.com',
	auth: { username: 'u', password: env.PW }
});

WebSocket

The WebSocket client uses the platform WebSocket API; the runtime handles TLS, framing, and masking.

import { connect } from 'edgeport/ws';

const ws = await connect('wss://stream.example.com/feed', { protocols: ['v1'] });
ws.send(JSON.stringify({ subscribe: 'ticks' }));

// directly iterate messages with `for await`
for await (const msg of ws) {
	if (msg.type === 'text') {
		const event = JSON.parse(msg.data);
		// handle the parsed event here
	}
}
const { code, reason } = await ws.closed;

JSON Helpers

import { connect } from 'edgeport/ws';

const ws = await connect('wss://stream.example.com/feed');
ws.sendJson({ subscribe: 'ticks' });
for await (const msg of ws) {
	if (msg.type === 'text') {
		const event = msg.json<{ price: number }>();
		// handle the parsed event here
	}
}

NATS

import { connect } from 'edgeport/nats';

await using nc = await connect({ hostname: 'nats.example.com', token: env.NATS_TOKEN });

// pub/sub
const sub = nc.subscribe('orders.*', { queue: 'workers' });
await nc.publish('orders.created', JSON.stringify({ id: 42 }));
for await (const msg of sub) {
	const order = JSON.parse(new TextDecoder().decode(msg.data));
	// handle the order here
}

// request-reply
const reply = await nc.request('time.now', '', { timeoutMs: 1000 });

NKey Authorization

nkey (ed25519) auth: pass nkeySeed (a SU... seed) instead of a token. For managed NATS (Synadia NGS) or any JWT-secured deployment, pass the contents of a .creds file - the user JWT and signing seed are extracted automatically:

import { connect } from 'edgeport/nats';

await using nc = await connect({ hostname: 'connect.ngs.global', creds: env.NATS_CREDS });

Subscribe with a Queue Group

Distribute messages across multiple workers by subscribing with a queue group:

import { connect } from 'edgeport/nats';

await using nc = await connect({ hostname: 'nats.example.com', token: env.NATS_TOKEN });
const sub = nc.subscribe('orders.*', { queue: 'workers' });

// directly iterate messages with `for await`
for await (const msg of sub) {
	const order = JSON.parse(new TextDecoder().decode(msg.data));
	// one member of the 'workers' queue group receives each message
}

JetStream

Durable, at-least-once streams via nc.jetstream(): ensure a stream, publish with a PubAck, and pull with a durable consumer that survives reconnects (un-acked messages are redelivered; acked ones are not).

await using nc = await connect({ hostname: 'nats.example.com', token: env.NATS_TOKEN });
const js = nc.jetstream();

await js.ensureStream('EVENTS', { subjects: ['events.>'] });
const ack = await js.publish('events.created', JSON.stringify({ id: 42 })); // { stream, seq }

const consumer = await js.pullSubscribe('EVENTS', 'worker-durable', { ackWaitMs: 30_000 });
for (const msg of await consumer.fetch(10, { expiresMs: 5000 })) {
	// process msg.data here
	await msg.ack(); // un-acked messages are redelivered after a reconnect
}

JSON Helpers

publishJson/requestJson and the per-message json()/text() accessors remove the encode/parse boilerplate:

import { connect } from 'edgeport/nats';

await using nc = await connect({ hostname: 'nats.example.com', token: env.NATS_TOKEN });

await nc.publishJson('readings', { temp: 21.5 });
for await (const { value } of nc.subscribeJson<{ temp: number }>('readings')) {
	console.log(value.temp);
}

const reply = await nc.requestJson<{ sum: number }>('calc.add', [2, 3]);

A subscribe responder replies with msg.respond(value); any received message exposes msg.json<T>() and msg.text().

MQTT

Raw TCP (1883 / TLS 8883) or over WebSocket - same API.

import { connect, connectWebSocket } from 'edgeport/mqtt';

await using mqtt = await connect({
	hostname: 'broker.example.com',
	port: 8883,
	tls: 'implicit',
	clientId: 'edge-worker-1',
	username: env.MQTT_USER,
	password: env.MQTT_PASS
});

await mqtt.publish('sensors/edge/temp', '21.4', { qos: 1, retain: true });
const sub = mqtt.subscribe('sensors/+/temp', { qos: 1 });
for await (const m of sub) console.log(m.topic, new TextDecoder().decode(m.payload));

// or tunnel MQTT through a WebSocket broker endpoint
const overWs = await connectWebSocket('wss://broker.example.com:8884/mqtt', {
	clientId: 'edge-ws'
});

Subscribe with Wildcards

Subscribe to topics with + (single-level) and # (multi-level) wildcards:

import { connect } from 'edgeport/mqtt';

await using mqtt = await connect({ hostname: 'broker.example.com', clientId: 'edge' });
const sub = mqtt.subscribe('sensors/+/temp', { qos: 1 });

for await (const m of sub) {
	console.log(m.topic, new TextDecoder().decode(m.payload));
}

Last Will and Persistent Sessions

Set a will and the broker publishes it if the client drops without a clean disconnect - the basis for presence / offline detection. Use cleanSession: false with a fixed clientId so queued QoS-1 messages are drained on reconnect.

await using device = await connect({
	hostname: 'broker.example.com',
	clientId: 'device-42',
	cleanSession: false, // persistent session: queued QoS>=1 messages survive a reconnect
	will: { topic: 'devices/42/status', payload: 'offline', qos: 1, retain: true }
});
await device.publish('devices/42/status', 'online', { retain: true });

// ... later, an unexpected drop publishes the will; a clean shutdown does not:
await device.close({ graceful: false }); // abrupt -> broker fires the 'offline' will

JSON Helpers

import { connect } from 'edgeport/mqtt';

await using mqtt = await connect({ hostname: 'broker.example.com', clientId: 'edge' });
await mqtt.publishJson('sensors/1', { temp: 21.5 }, { qos: 1 });
for await (const { topic, value } of mqtt.subscribeJson<{ temp: number }>('sensors/+', {
	qos: 1
})) {
	console.log(topic, value.temp);
}

A plain subscribe message also exposes msg.json<T>() and msg.text().

STOMP

import { connect } from 'edgeport/stomp';

await using stomp = await connect({
	hostname: 'mq.example.com',
	login: env.MQ_USER,
	passcode: env.MQ_PASS
});

await stomp.send('/queue/jobs', JSON.stringify({ task: 'resize' }));
const sub = stomp.subscribe('/queue/jobs', { ack: 'client' });
for await (const m of sub) {
	// process the job (m.body) here
	await m.ack?.();
}

Heartbeats

Cloud messaging brokers often require heartbeats to keep the connection alive. edgeport supports STOMP heartbeats:

import { connect } from 'edgeport/stomp';

await using stomp = await connect({
	hostname: 'mq.example.com',
	login: env.MQ_USER,
	passcode: env.MQ_PASS,
	heartBeat: [10000, 10000] // [send, expect] in ms; negotiated down with the broker
});

Transactions

Stage sends in a transaction; the broker releases them only on commit() and discards them on abort().

const tx = await stomp.begin();
await tx.send('/queue/orders', JSON.stringify(order));
await tx.send('/queue/audit', JSON.stringify(entry));
if (ok)
	await tx.commit(); // both messages delivered atomically
else await tx.abort(); // neither is ever delivered

JSON Helpers

import { connect } from 'edgeport/stomp';

await using stomp = await connect({
	hostname: 'mq.example.com',
	login: env.MQ_USER,
	passcode: env.MQ_PASS
});
await stomp.sendJson('/queue/jobs', { job: 'reindex', n: 7 }); // sets content-type: application/json
const sub = stomp.subscribe('/queue/jobs');
for await (const msg of sub) {
	const job = msg.json<{ job: string; n: number }>();
	// handle the job here
}

FTP

Plaintext FTP, passive mode (Workers cannot accept the inbound connections active mode needs).

import { connect, getFile, putFile } from 'edgeport/ftp';

await using ftp = await connect({
	hostname: 'files.example.com',
	username: 'u',
	password: env.FTP_PW
});
await ftp.put('reports/today.csv', new TextEncoder().encode('a,b,c\n'));
for (const entry of await ftp.list('reports')) console.log(entry.name, entry.size);
const bytes = await ftp.get('reports/today.csv');

ASCII Mode and Resume

get/put take an options object: type: 'ascii' issues TYPE A (line-ending conversion) vs the default 'binary' (TYPE I); offset issues REST <n> to resume a download, and append: true resumes an upload (APPE) - the practical recovery path after a dropped data channel.

// resume an interrupted upload from where it stopped, and download just the tail
await ftp.put('big.bin', firstChunk);
await ftp.put('big.bin', restOfFile, { append: true });
const tail = await ftp.get('big.bin', { offset: firstChunk.length });

// transfer a text file in ASCII mode
await ftp.put('records.txt', data, { type: 'ascii' });

Convenience Helpers

Higher-level helpers over the raw commands; paths resolve relative to the working directory unless absolute.

import { connect } from 'edgeport/ftp';

await using ftp = await connect({
	hostname: 'files.example.com',
	username: 'u',
	password: env.FTP_PW
});

if (await ftp.exists('/etc/app/config.json')) {
	const cfg = await ftp.getJson<{ name: string }>('/etc/app/config.json');
}

await ftp.ensureDir('/incoming/2026/reports'); // recursive mkdir, one MKD per segment
await ftp.putText('/incoming/note.txt', 'hello world\n');
const note = await ftp.getText('/incoming/note.txt');
const when = await ftp.mtime('/incoming/note.txt'); // MDTM, parsed as UTC

await ftp.removeAll('/incoming/2026'); // recursive, client-side, non-atomic; refuses '' and '/'

LDAP / LDAPS

import { connect } from 'edgeport/ldap';
// or: import { connect } from 'edgeport/ldaps'  // implicit TLS on 636

await using ldap = await connect({
	hostname: 'ldap.example.com',
	bindDN: 'cn=svc,dc=example,dc=org',
	password: env.LDAP_PW
});

const users = await ldap.search({
	base: 'ou=people,dc=example,dc=org',
	scope: 'sub',
	filter: '(&(objectClass=person)(mail=*@example.org))',
	attributes: ['cn', 'mail']
});
for (const u of users) console.log(u.dn, u.attributes.mail);

StartTLS

LDAP StartTLS is supported on the standard LDAP port (389):

import { connect } from 'edgeport/ldap';

await using ldap = await connect({
	hostname: 'ldap.example.com',
	port: 389,
	tls: 'starttls',
	bindDN: 'cn=svc,dc=example,dc=org',
	password: env.LDAP_PW
});

const entries = await ldap.search({
	base: 'ou=people,dc=example,dc=org',
	scope: 'sub',
	filter: '(uid=jdoe)',
	attributes: ['cn', 'mail']
});

LDAP Filters

LDAP search filters are expressed in RFC 4515 syntax:

import { connect } from 'edgeport/ldap';

await using ldap = await connect({
	hostname: 'ldap.example.com',
	bindDN: 'cn=svc,dc=example,dc=org',
	password: env.LDAP_PW
});

const users = await ldap.search({
	base: 'ou=people,dc=example,dc=org',
	scope: 'sub',
	filter: '(&(objectClass=person)(mail=*@example.org))',
	attributes: ['cn', 'mail']
});

Filter Builders

Build search filters from untrusted input safely. The structured builders carry values literally, so a *, (, or ) in user input becomes a literal byte on the wire and can never inject filter syntax. Drop the result straight into search({ filter }) / findOne.

import { connect, and, eq, present } from 'edgeport/ldap';

await using ldap = await connect({
	hostname: 'ldap.example.com',
	bindDN: 'cn=svc,dc=example,dc=org',
	password: env.LDAP_PW
});

const users = await ldap.search({
	base: 'ou=people,dc=example,dc=org',
	filter: and(eq('objectClass', 'person'), present('mail')) // (&(objectClass=person)(mail=*))
});
const one = await ldap.findOne({
	base: 'ou=people,dc=example,dc=org',
	filter: eq('uid', userInput)
});

Builders: and, or, not, eq, present, gte, lte, approx, substring, contains (also grouped under a filters namespace). When you assemble a filter string or DN by hand instead, escape the interpolated value with escapeFilterValue / escapeDN.

authenticate does the bind-search-bind verify flow in one call:

import { authenticate, eq } from 'edgeport/ldap';

const entry = await authenticate({
	hostname: 'ldap.example.com',
	bindDN: 'cn=svc,dc=example,dc=org',
	bindPassword: env.SVC_PW,
	base: 'ou=people,dc=example,dc=org',
	userFilter: eq('uid', username),
	password: submittedPassword
});
if (entry) {
	// authenticated; entry.dn is the bound user
}

Syslog

import { connect, Severity } from 'edgeport/syslog';

await using log = await connect({
	hostname: 'logs.example.com',
	port: 6514,
	tls: 'implicit',
	appName: 'edge-worker'
});
await log.log({
	severity: Severity.info,
	message: 'request handled',
	structuredData: [{ id: 'req@1', params: { ms: '12' } }]
});

Severity Shortcuts

info/notice/warn/error/debug delegate to log() with the matching severity:

import { connect } from 'edgeport/syslog';

await using log = await connect({ hostname: 'logs.example.com', appName: 'edge-worker' });
await log.info('request handled');
await log.warn('disk almost full', { facility: 'local0' });
await log.error('request failed', { structuredData: [{ id: 'req@1', params: { code: '500' } }] });

SMPP

Send and receive SMS through a carrier's SMSC over SMPP v3.4. Bind as a transmitter, receiver, or transceiver; submit messages; and iterate inbound deliver_sms - mobile-originated messages and delivery receipts - over the same session.

import { connect } from 'edgeport/smpp';

await using smpp = await connect({
	hostname: 'smsc.example.com',
	port: 2775,
	systemId: 'esme',
	password: env.SMPP_PW,
	bindMode: 'transceiver'
});

// submit and get the SMSC message id back; request a delivery receipt
const id = await smpp.submit({
	source: 'EDGEPORT',
	destination: '12065550111',
	message: 'hello from the edge',
	registeredDelivery: true
});

// iterate inbound messages and delivery receipts
for await (const inbound of smpp.messages()) {
	if (inbound.isDeliveryReceipt) {
		const r = inbound.receipt();
		if (r.id === id) console.log('delivered:', r.stat); // e.g. 'DELIVRD'
	} else {
		console.log('MO from', inbound.source, inbound.text());
	}
}

One-shot Send SMS

import { sendMessage } from 'edgeport/smpp';

const id = await sendMessage({
	hostname: 'smsc.example.com',
	systemId: 'esme',
	password: env.SMPP_PW,
	source: 'EDGEPORT',
	destination: '12065550111',
	message: 'one-shot SMS from a Worker'
});

Unicode and Long Messages

Pass dataCoding: DataCoding.Ucs2 for non-GSM text (encoded UTF-16BE). A body over 254 octets is carried in a message_payload TLV automatically.

import { connect, DataCoding } from 'edgeport/smpp';

await using smpp = await connect({
	hostname: 'smsc.example.com',
	systemId: 'esme',
	password: env.SMPP_PW
});
await smpp.submit({
	destination: '12065550111',
	message: 'こんにちは',
	dataCoding: DataCoding.Ucs2
});

TLS is implicit-only (tls: 'implicit'); SMPP has no in-band STARTTLS. The session sends periodic enquire_link keep-alives and answers the SMSC's, and close() unbinds cleanly.

SIP (RCS)

A SIP user agent over raw TCP/TLS: register, send and receive messages, probe capabilities, subscribe to presence, and run MSRP chat sessions. SIP + MSRP are the protocols the chat side of RCS (the GSMA Universal Profile) rides on, so this is edgeport's RCS-family client - but it targets open SIP infrastructure (Asterisk, FreeSWITCH, Kamailio, OpenSIPS, and cloud SIP trunks like Twilio / Telnyx / Flowroute), not carrier RCS.

[!NOTE] This is a signaling + messaging client. It does not carry voice/video media (RTP/SRTP is UDP, which Workers cannot open), and it does not reach carrier-RCS handsets - that path is gated behind IMS/SIM provisioning, and for businesses it is an HTTPS REST API (Google Jibe / aggregators), which a Worker already calls with fetch. TLS is implicit-only (tls: 'implicit'); SIP has no in-band STARTTLS.

One-shot Message

A single MESSAGE needs no registration - the server challenges it, edgeport authenticates, and the proxy routes it - so it fits a normal request-scoped Worker.

import { sendMessage } from 'edgeport/sip';

await sendMessage({
	hostname: 'sip.example.com',
	username: 'alice',
	password: env.SIP_PW,
	to: 'bob',
	text: 'hello over SIP'
});

Register and Receive

To receive inbound messages, REGISTER with RFC 5626 "outbound", which lets a listen-less Worker receive requests on its own connection (the same trick SMPP deliver_sm uses). Hold the session in a Durable Object to keep the registration open: an open socket keeps a DO alive up to ~15 minutes, and the session refreshes REGISTER on its own; reconnect on a DO alarm.

import { connect } from 'edgeport/sip';

await using ua = await connect({
	hostname: 'sip.example.com',
	username: 'alice',
	password: env.SIP_PW
});
await ua.register();
await ua.message('bob', 'hi bob');
for await (const m of ua.messages()) {
	console.log('from', m.from, m.text());
}

Capabilities and Presence

const caps = await ua.options('bob'); // { status, allow: [...], accept: [...] }

await using sub = await ua.subscribePresence('bob');
for await (const note of sub) {
	console.log(note.state, note.text()); // Subscription-State + PIDF XML
}

MSRP Chat (session mode)

invite() offers an MSRP message session and, on answer, opens the chat over MSRP (RFC 4975); the Worker is always the active side and dials the peer's MSRP path.

await using chat = await ua.invite('bob');
await chat.send('rich chat over MSRP');
for await (const m of chat.messages()) console.log(m.text());

Digest auth (RFC 2617 / 7616) is handled transparently, including the MD5 that Workers WebCrypto lacks - assembled and KAT-verified in the module, the same way SSH assembles ChaCha.

Utilities

edgeport/util is a small set of transport-free helpers the protocol modules share, published for consumers too: byte-encoding codecs, random ids off the CSPRNG, and a retry-with-backoff tuned to edgeport's error vocabulary.

import { toHex, fromHex, toBase64, fromBase64, randomId, retry } from 'edgeport/util';

toHex(new Uint8Array([0xde, 0xad])); // 'dead'
toBase64(bytes, { urlSafe: true }); // url-safe, unpadded
fromBase64('data:text/plain;base64,aGk='); // tolerant: strips the data-uri prefix, restores padding
randomId('worker'); // 'worker-3f9a...'

// retry only transient ConnectionError / TimeoutError - never AuthError / ProtocolError
import { connect } from 'edgeport/ssh';
const ssh = await retry(() => connect({ hostname: 'box', username: 'u', password: env.PW }), {
	attempts: 4
});

Real-World Recipes

[!NOTE] For larger multi-protocol workflows - mail automation, secure deploy/ops, an HL7 integration engine, device fleet management, resilience/recovery, and more - see ADVANCED_USAGE.md, each backed by an end-to-end integration test in test/integration/recipes/.

A cron Worker that runs a Remote Command

import { exec } from 'edgeport/ssh';

export default {
	async scheduled(_event, env) {
		const { code } = await exec({
			hostname: env.BOX,
			username: 'deploy',
			privateKey: { pem: env.SSH_KEY },
			command: 'systemctl restart myapp'
		});
		if (code !== 0) throw new Error('restart failed');
	}
};

Email an Alert on a Webhook

import { send } from 'edgeport/smtp';

export default {
	async fetch(req, env) {
		const body = await req.text();
		await send({
			hostname: env.SMTP_HOST,
			auth: { username: env.SMTP_USER, password: env.SMTP_PW },
			from: env.SMTP_USER,
			to: env.ONCALL,
			subject: 'Webhook alert',
			text: body
		});
		return new Response('ok');
	}
};

Poll a Mailbox and Archive Attachments to SFTP

import { connect as imapConnect } from 'edgeport/imap';
import { putFile } from 'edgeport/sftp';

await using imap = await imapConnect({
	hostname: env.IMAP,
	auth: { username: env.U, password: env.P }
});
await imap.select('INBOX');
const uids = await imap.search({ unseen: true });
const messages = await imap.fetch(uids, { body: true });
for (const m of messages) {
	if (m.body)
		await putFile({
			hostname: env.SFTP,
			username: env.U,
			password: env.P,
			path: `/archive/${m.uid}.eml`,
			data: m.body
		});
}

Run a Remote Command and Publish the Result to NATS

import { connect as sshConnect } from 'edgeport/ssh';
import { connect as natsConnect } from 'edgeport/nats';

await using ssh = await sshConnect({
	hostname: env.BOX,
	username: 'deploy',
	privateKey: { pem: env.SSH_KEY }
});
const disk = await ssh.df('/'); // parsed rows: { filesystem, sizeKb, usedKb, availKb, ... }
await using nc = await natsConnect({ hostname: env.NATS, token: env.NATS_TOKEN });
await nc.publishJson('telemetry.disk', disk); // JSON-encodes the structured usage

Bridge MQTT Sensor Readings to Syslog (edge observability)

import { connect as mqttConnect } from 'edgeport/mqtt';
import { connect as syslogConnect, Severity } from 'edgeport/syslog';

await using mqtt = await mqttConnect({ hostname: env.BROKER, clientId: 'edge-bridge' });
await using log = await syslogConnect({
	hostname: env.SIEM,
	port: 6514,
	tls: 'implicit',
	appName: 'sensors'
});

for await (const reading of mqtt.subscribe('sensors/#', { qos: 1 })) {
	await log.log({
		severity: Severity.info,
		message: `${reading.topic}=${new TextDecoder().decode(reading.payload)}`
	});
}

Authorize an Action via LDAP, then Alert over STOMP

import { connect as ldapConnect } from 'edgeport/ldaps';
import { eq } from 'edgeport/ldap';
import { connect as stompConnect } from 'edgeport/stomp';

await using dir = await ldapConnect({
	hostname: env.LDAP,
	bindDN: env.SVC_DN,
	password: env.SVC_PW
});
// eq() carries the value literally, so an untrusted uid can't inject filter syntax
const allowed = await dir.search({ base: 'ou=people,dc=example,dc=org', filter: eq('uid', uid) });

if (allowed.length === 0) {
	await using mq = await stompConnect({
		hostname: env.MQ,
		login: env.MQ_USER,
		passcode: env.MQ_PW
	});
	await mq.send('/queue/security.alerts', `unauthorized action by ${uid}`);
}

Pull a File over FTP and Republish to MQTT

import { getFile } from 'edgeport/ftp';
import { connect as mqttConnect } from 'edgeport/mqtt';

const csv = await getFile({
	hostname: env.FTP,
	username: env.U,
	password: env.P,
	path: '/exports/prices.csv'
});
await using mqtt = await mqttConnect({ hostname: env.BROKER, clientId: 'price-feed' });
await mqtt.publish('feeds/prices', csv, { qos: 1, retain: true });

Error Handling

Every edgeport call rejects with one of four types, all extending EdgeportError:

import { AuthError, ConnectionError, ProtocolError, TimeoutError } from 'edgeport';

try {
  await exec({ hostname: 'h', username: 'u', password: 'wrong', command: 'id' });
} catch (err) {
  if (err instanceof AuthError) // bad credentials
  else if (err instanceof ConnectionError) // could not reach / TLS / dropped
  else if (err instanceof ProtocolError) // malformed or unsupported (e.g. no common cipher)
  else if (err instanceof TimeoutError) // a deadline elapsed
}

Limitations

  • No port 25. Cloudflare blocks outbound port 25; use 587 or 465 for SMTP.
  • ChaCha20 throughput. [email protected] is pure-JS (via @noble/ciphers) and slower than the hardware-paced AES-GCM that WebCrypto provides; AES-GCM is preferred during negotiation. Watch Worker CPU limits on very large ChaCha transfers.
  • No agent forwarding or X11. Local port forwarding is supported via forwardOut (direct-tcpip reach-through); SOCKS (-D) and remote (-R) forwarding are out of scope because Workers cannot accept inbound connections.
  • Encrypted private keys: encrypted PKCS#8 (PBES2) and OpenSSH-format keys are supported; legacy DEK-Info PEM and aes-gcm/chacha20-poly1305 OpenSSH key ciphers are not (re-encrypt with aes256-ctr).
  • Time-based rekey is not triggered (byte-volume and server-initiated rekeys are).
  • FTPS is not provided. The Workers startTls API exposes no TLS session export/import, so the FTPS data connection cannot resume the control channel's TLS session - which strict servers (e.g. vsftpd require_ssl_reuse=YES) mandate. Shipping it would only work against servers that disable that protection, so plain edgeport/ftp is provided and FTPS is deferred until the runtime gains TLS session control.
  • FTP is passive-mode only (Workers cannot accept the inbound connections active mode needs).
  • LDAP SASL is not implemented (simple bind over TLS is). The one mechanism that would add capability over simple bind, SASL EXTERNAL, needs a TLS client certificate, which the Workers socket API cannot present; PLAIN is equivalent to simple bind, and SCRAM/DIGEST/GSSAPI are rarely required. NATS auth is fully covered (token / user-pass / nkey / JWT); NATS does not use SASL. MQTT/STOMP/Syslog authenticate via TLS + username-password.

Advanced: Building-Block Exports

Beyond the protocol modules, edgeport publishes the lower-level SSH building blocks it is assembled from, for tooling that needs them directly:

| Import | Provides | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------- | | edgeport/wire | SSH binary wire codecs (SshReader, SshWriter, toMpintBody) | | edgeport/crypto | hashes/HMAC, the SSH packet ciphers (createPacketCipher, cipherSizes), host/user keys (verifyHostSignature, loadUserKey) | | edgeport/kex | KEXINIT negotiation, the exchange hash + key schedule, createKex, and the curve25519/nistp256 namespaces | | edgeport/auth | SSH user authentication (authenticate) |

import { SshReader, SshWriter } from 'edgeport/wire';
import { createPacketCipher, verifyHostSignature } from 'edgeport/crypto';
import { negotiate, createKex } from 'edgeport/kex';

These are stable but lower-level; most applications only need the protocol modules above.

API Reference

Full generated API docs: see the published TypeDoc site (built from source by bun run docs:build).

Contributing

bun install
bun run typecheck
bun run test                                   # gate: unit + KAT, hermetic
docker compose -f docker/compose.yml up -d --wait
INTEGRATION=1 bun run test                     # integration under workerd vs real servers
docker compose -f docker/compose.yml down -v

License

MIT (c) Gregory Mitchell 2026. See LICENSE file for details.