{"id":51632912,"url":"https://github.com/gmitch215/edgeport","last_synced_at":"2026-07-13T10:30:26.313Z","repository":{"id":368161752,"uuid":"1283585415","full_name":"gmitch215/edgeport","owner":"gmitch215","description":"🛜 TCP Library for Cloudflare Workers","archived":false,"fork":false,"pushed_at":"2026-07-08T04:29:56.000Z","size":2430,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-08T06:10:55.383Z","etag":null,"topics":["cloudflare","cloudflare-workers","crypto","cryptography","javascript","sftp","ssh","tcp","typescript","websocket"],"latest_commit_sha":null,"homepage":"https://gmitch215.github.io/edgeport/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gmitch215.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"patreon":"gmitch215","liberapay":"gmitch215","buy_me_a_coffee":"gmitch215"}},"created_at":"2026-06-29T03:59:51.000Z","updated_at":"2026-06-30T07:19:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gmitch215/edgeport","commit_stats":null,"previous_names":["gmitch215/edgeport"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/gmitch215/edgeport","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmitch215%2Fedgeport","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmitch215%2Fedgeport/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmitch215%2Fedgeport/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmitch215%2Fedgeport/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gmitch215","download_url":"https://codeload.github.com/gmitch215/edgeport/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gmitch215%2Fedgeport/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35420322,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-13T02:00:06.543Z","response_time":119,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","cloudflare-workers","crypto","cryptography","javascript","sftp","ssh","tcp","typescript","websocket"],"created_at":"2026-07-13T10:30:25.554Z","updated_at":"2026-07-13T10:30:26.304Z","avatar_url":"https://github.com/gmitch215.png","language":"TypeScript","funding_links":["https://patreon.com/gmitch215","https://liberapay.com/gmitch215","https://buymeacoffee.com/gmitch215"],"categories":[],"sub_categories":[],"readme":"# edgeport\n\n\u003e 🛜 TCP Library for Cloudflare Workers\n\nedgeport gives Cloudflare Workers native clients for various TCP protocols, built directly on `cloudflare:sockets`.\n\nNode libraries assume Node's `net`/`tls` and Node crypto, none of which exist in workerd;\nedgeport is written for the Workers runtime from the ground up.\n\n## Table of Contents\n\n- [Why edgeport](#why-edgeport)\n- [Features](#features)\n- [Install](#install)\n- [Getting Started](#getting-started)\n- [SSH](#ssh)\n- [SFTP](#sftp)\n- [SMTP](#smtp)\n- [IMAP](#imap)\n- [POP3](#pop3)\n- [WebSocket](#websocket)\n- [NATS](#nats)\n- [MQTT](#mqtt)\n- [STOMP](#stomp)\n- [FTP](#ftp)\n- [LDAP / LDAPS](#ldap--ldaps)\n- [Syslog](#syslog)\n- [Real-World Recipes](#real-world-recipes)\n- [Error Handling](#error-handling)\n- [Limitations](#limitations)\n- [API Reference](#api-reference)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Why edgeport\n\nWorkers can open raw TCP sockets, but every existing SSH/mail client depends on Node APIs\nthe runtime does not provide. edgeport is:\n\n- **Workers-Native.** One small core is the only code that imports `cloudflare:sockets`;\n  every protocol builds on it. Nothing depends on Node.\n- **TypeScript-first.** The types are the contract and the documentation.\n- **Tree-Shakeable.** Import only the protocol you use (`edgeport/ssh`, `edgeport/smtp`, ...).\n- **Tested against Real Servers.** Every protocol is verified under `workerd` against\n  Dockerized servers (OpenSSH, Dropbear, GreenMail, NATS with JetStream, Mosquitto, ActiveMQ,\n  OpenLDAP, an FTP server, and a WebSocket echo server) - not mocks.\n\n## Features\n\n- **SSH** transport, auth, and connection layers: `exec`, interactive `shell`, subsystems.\n- **SFTP** v3 over SSH: read/write/list/stat/rename/remove, plus streaming.\n- **SMTP** send with STARTTLS / implicit TLS and AUTH PLAIN/LOGIN.\n- **IMAP** and **POP3** read access with STARTTLS / implicit TLS.\n- **WebSocket** client via the platform API (`for await` message iteration).\n- **NATS** pub/sub, request-reply, and queue groups; token / user-pass / nkey / JWT (creds) auth.\n- **MQTT** v3.1.1 (QoS 0/1/2, keep-alive, wildcards) over raw TCP or WebSocket.\n- **STOMP** 1.2 messaging (send / subscribe / ack) for ActiveMQ, RabbitMQ, and friends.\n- **FTP** (plaintext, passive mode): upload, download, list, and directory ops.\n- **LDAP / LDAPS** simple bind + search with RFC 4515 filters (BER codec, StartTLS).\n- **Syslog** RFC 5424 over TCP/TLS with octet-counting or LF framing.\n- One uniform error vocabulary: `AuthError`, `ConnectionError`, `ProtocolError`, `TimeoutError`.\n\n### SSH algorithm support\n\n| Category         | Supported                                                                                                       |\n| ---------------- | --------------------------------------------------------------------------------------------------------------- |\n| Key Exchange     | `curve25519-sha256`, `ecdh-sha2-nistp256`                                                                       |\n| Host / User Keys | Ed25519, ECDSA-P256, RSA-SHA2-256/512                                                                           |\n| Ciphers          | `aes256-gcm@openssh.com`, `aes128-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes256-ctr`, `aes128-ctr` |\n| MAC              | `hmac-sha2-256`, `hmac-sha2-512` (AEAD Ciphers carry their own)                                                 |\n| Auth             | publickey, password, keyboard-interactive                                                                       |\n\nChaCha20-Poly1305 is assembled from [`@noble/ciphers`](https://github.com/paulmillr/noble-ciphers)\nbecause Workers WebCrypto does not provide it.\n\n## Install\n\n```sh\nbun add edgeport\n# or: npm install edgeport\n```\n\nedgeport targets the Workers runtime. Use it inside a Worker, not in plain Node.\n\n## Getting Started\n\n```typescript\nimport { exec } from 'edgeport/ssh';\n\nexport default {\n\tasync fetch(): Promise\u003cResponse\u003e {\n\t\tconst { stdout, code } = await exec({\n\t\t\thostname: 'example.com',\n\t\t\tusername: 'deploy',\n\t\t\tpassword: env.SSH_PASSWORD,\n\t\t\tcommand: 'uptime'\n\t\t});\n\t\treturn new Response(`exit ${code}\\n${new TextDecoder().decode(stdout)}`);\n\t}\n};\n```\n\n## SSH\n\n### One-shot Exec\n\n```typescript\nimport { exec } from 'edgeport/ssh';\n\nconst { stdout, stderr, code } = await exec({\n\thostname: 'host',\n\tusername: 'user',\n\tprivateKey: { pem: env.SSH_KEY }, // PKCS8 PEM, or pass a CryptoKey\n\tcommand: 'ls -la /var/log'\n});\n```\n\n### A reusable Session\n\n```typescript\nimport { connect } from 'edgeport/ssh';\n\nawait using ssh = await connect({ hostname: 'host', username: 'user', password: env.PW });\nconst a = await ssh.exec('hostname');\nconst b = await ssh.exec('date');\n// the session closes automatically at the end of the `await using` scope\n```\n\n### Interactive Shell (streaming)\n\n```typescript\nawait using ssh = await connect({ hostname: 'host', username: 'user', password: env.PW });\nawait using shell = await ssh.shell();\n\nawait shell.write(new TextEncoder().encode('echo hi\\n'));\nconst reader = shell.stdout.getReader();\nconst { value } = await reader.read();\nconsole.log(new TextDecoder().decode(value));\n```\n\n### Keyboard-Interactive and Host-Key Pinning\n\n```typescript\nawait using ssh = await connect({\n\thostname: 'host',\n\tusername: 'user',\n\tonKeyboardInteractive: async (prompts) =\u003e prompts.map(() =\u003e env.OTP),\n\thostKey: {\n\t\tverify: (type, key) =\u003e type === 'ssh-ed25519' /* \u0026\u0026 key matches your pinned host key */\n\t}\n});\n```\n\n### Executing with Sudo\n\n`sudo` does not read its password from SSH auth - it prompts over the channel. The `sudo`\nhelpers run `sudo -S -p ''` (read the password from stdin, silence sudo's own prompt) and\nfeed it the secret for you. The sudoers policy must allow password sudo without a tty\n(`requiretty` off, the default on most modern distros).\n\nOne-shot over a fresh connection - `sudoPassword` defaults to `password`, so the SSH login\npassword is reused as the sudo password (the common case):\n\n```typescript\nimport { sudoExec } from 'edgeport/ssh';\n\nconst { stdout, code } = await sudoExec({\n\thostname: 'host',\n\tusername: 'user',\n\tpassword: env.PW,\n\tcommand: 'systemctl restart myapp'\n});\nconsole.log(code, new TextDecoder().decode(stdout));\n```\n\nReusing an already-open session (pass the sudo password explicitly):\n\n```typescript\nimport { connect, sudo } from 'edgeport/ssh';\n\nawait using ssh = await connect({ hostname: 'host', username: 'user', password: env.PW });\nconst { stdout } = await sudo(ssh, 'whoami', { password: env.PW });\nconsole.log(new TextDecoder().decode(stdout)); // \"root\"\n```\n\n### Forcing a Cipher\n\n```typescript\nawait using ssh = await connect({\n\thostname: 'host',\n\tusername: 'user',\n\tpassword: env.PW,\n\talgorithms: { cipher: ['chacha20-poly1305@openssh.com'] }\n});\n```\n\n### Encrypted (Passphrase-Protected) Keys\n\nEncrypted PKCS#8 (`BEGIN ENCRYPTED PRIVATE KEY`) and OpenSSH-format keys (`BEGIN OPENSSH\nPRIVATE KEY`, the `ssh-keygen` default), encrypted or not, are accepted - pass the\n`passphrase`:\n\n```typescript\nawait using ssh = await connect({\n\thostname: 'host',\n\tusername: 'user',\n\tprivateKey: { pem: env.SSH_KEY, passphrase: env.SSH_KEY_PASSPHRASE }\n});\n```\n\n### Key Re-Exchange (Rekey)\n\nLong-lived sessions and large transfers rekey automatically (default every ~1 GiB, per\nRFC 4253 §9); server-initiated rekeys are handled transparently. Tune or force it:\n\n```typescript\nawait using ssh = await connect({\n\thostname: 'host',\n\tusername: 'user',\n\tpassword: env.PW,\n\trekeyThresholdBytes: 256 * 1024 * 1024 // auto-rekey every 256 MiB (0 disables)\n});\nawait ssh.rekey(); // or force one now\n```\n\n### Port Forwarding (Tunneling)\n\n`forwardOut` opens a `direct-tcpip` channel: the SSH server connects to a target on your\nbehalf and pipes the bytes back, so a Worker can reach a service that isn't internet-exposed\nthrough an SSH bastion (the `-L` reach-through). It returns a duplex channel - `stdout` is\ninbound bytes, `write()` sends outbound.\n\n```typescript\nimport { connect } from 'edgeport/ssh';\n\nawait using ssh = await connect({\n\thostname: 'bastion',\n\tusername: 'u',\n\tprivateKey: { pem: env.SSH_KEY }\n});\n\n// reach an internal-only Postgres that sits behind the bastion\nawait using tunnel = await ssh.forwardOut('10.0.0.5', 5432);\nawait tunnel.write(startupPacket);\nfor await (const chunk of tunnel.stdout) {\n\t// handle each inbound chunk from the tunneled service here\n}\n```\n\nWorkers cannot accept inbound connections, so the listening half of `-L`/`-D` (a local SOCKS\nlistener) and remote forwarding (`-R`) are out of scope - the server-side reach-through is the\nvaluable part and is fully supported. The server must permit TCP forwarding\n(`AllowTcpForwarding`).\n\n### SSH Command Helpers\n\nOn top of `exec`/`execStream`, a session carries ergonomic shell helpers. Each one\nsingle-quotes the paths and arguments it interpolates, so a space or `$` in a path cannot\nbreak the command; the destructive `rm` also refuses obviously dangerous targets (`/`, `~`,\n`.`, `..`).\n\nThese helpers assume a **POSIX shell**, so they work against Linux, macOS, and other Unix\nremotes - `stat` and `spawnDetached` handle the GNU/BSD differences (GNU `stat -c` vs BSD\n`stat -f`; `nohup` rather than Linux-only `setsid`). For a Windows (`cmd.exe`/PowerShell) or\nother non-POSIX remote, drop down to `run`/`exec`/`execStream` with native commands.\n\n```typescript\nimport { connect } from 'edgeport/ssh';\n\nawait using ssh = await connect({ hostname: 'host', username: 'user', password: env.PW });\n\nconst host = await ssh.run('hostname'); // decoded, trimmed stdout; throws on a nonzero exit\nif (await ssh.test('command -v docker')) {\n\t// docker is installed\n}\nif (await ssh.exists('/etc/hosts')) {\n\t// the file is there\n}\n```\n\n```typescript\n// files and directories\nawait ssh.mkdirp('/srv/app/releases', { mode: 0o755 });\nawait ssh.writeTextFile('/srv/app/.env', 'PORT=8080\\n');\nconst envFile = await ssh.readTextFile('/srv/app/.env');\nawait ssh.chmod('/srv/app/run.sh', 0o755);\nconst st = await ssh.stat('/srv/app'); // { size, mode, mtime, isDirectory, isSymlink }\nawait ssh.rm('/srv/app/tmp', { recursive: true, force: true });\n\n// system introspection and background work\nconst usage = await ssh.df('/srv'); // [{ filesystem, sizeKb, usedKb, availKb, usePercent, mountedOn }]\nconst git = await ssh.which('git'); // '/usr/bin/git' | null\nawait ssh.spawnDetached('/srv/app/worker', { stdout: '/var/log/worker.log' });\n```\n\nFor one-call workflows there are connect-run-close one-shots:\n\n```typescript\nimport { run, test, exists } from 'edgeport/ssh';\n\nconst out = await run({ hostname: 'host', username: 'user', password: env.PW, command: 'uptime' });\nconst ok = await exists({\n\thostname: 'host',\n\tusername: 'user',\n\tpassword: env.PW,\n\tpath: '/etc/hosts'\n});\n```\n\n## SFTP\n\n```typescript\nimport { connect, getFile, putFile } from 'edgeport/sftp';\n\n// one-shots\nconst bytes = await getFile({ hostname: 'h', username: 'u', password: p, path: '/etc/hostname' });\nawait putFile({ hostname: 'h', username: 'u', password: p, path: '/tmp/x', data: bytes });\n\n// a session\nawait using sftp = await connect({ hostname: 'h', username: 'u', password: p });\nawait sftp.mkdir('/tmp/reports');\nawait sftp.writeFile('/tmp/reports/today.csv', new TextEncoder().encode('a,b,c\\n'));\nfor (const entry of await sftp.list('/tmp/reports')) {\n\tconsole.log(entry.filename, entry.attrs.size);\n}\n\n// stream a large download\nconst stream = sftp.createReadStream('/var/log/big.log');\n```\n\n### Reusing an Existing Session\n\nReuse an existing SSH session instead of opening a second connection:\n\n```typescript\nimport { connect as sshConnect } from 'edgeport/ssh';\nimport { connect as sftpConnect } from 'edgeport/sftp';\n\nawait using ssh = await sshConnect({ hostname: 'h', username: 'u', password: p });\nawait using sftp = await sftpConnect({ session: ssh });\n```\n\n### Large File Transfers\n\nFor large files, use the streaming API to avoid buffering the entire file in memory:\n\n```typescript\nawait using sftp = await connect({ hostname: 'h', username: 'u', password: p });\nconst readStream = sftp.createReadStream('/var/log/big.log');\nconst writeStream = sftp.createWriteStream('/tmp/bigfile');\n```\n\n### SFTP Convenience Helpers\n\nHigher-level helpers built on the same request/response framing:\n\n```typescript\nimport { connect } from 'edgeport/sftp';\n\nawait using sftp = await connect({ hostname: 'h', username: 'u', password: p });\n\n// presence check (a \"no such file\" status resolves false; other errors propagate)\nif (!(await sftp.exists('/srv/app'))) {\n\tawait sftp.ensureDir('/srv/app/releases/2026'); // recursive mkdir, one level per segment\n}\n\n// text and JSON round-trips (UTF-8)\nawait sftp.writeText('/srv/app/note.txt', 'deployed\\n');\nconst note = await sftp.readText('/srv/app/note.txt');\nawait sftp.writeJson('/srv/app/config.json', { port: 8080 }, { space: 2 });\nconst config = await sftp.readJson\u003c{ port: number }\u003e('/srv/app/config.json');\n\nawait sftp.chmod('/srv/app/run.sh', 0o755);\n\n// removal: one empty dir, a batch of files, or a whole tree\nawait sftp.rmdir('/srv/app/empty');\nawait sftp.removeMany(['/tmp/a.log', '/tmp/b.log'], { ignoreMissing: true });\nawait sftp.removeAll('/srv/app/releases/old'); // recursive, client-side, non-atomic\n```\n\n`removeAll` walks the tree from the client (one round trip per entry) and refuses an empty\npath or `/`; `ensureDir` tolerates segments that already exist and verifies the leaf is a\ndirectory.\n\n## SMTP\n\n**Port 25 is blocked on Workers**, so edgeport sends over submission (587, STARTTLS) or\nimplicit TLS (465).\n\n```typescript\nimport { send } from 'edgeport/smtp';\n\nawait send({\n\thostname: 'smtp.example.com',\n\tport: 587, // STARTTLS (default)\n\tauth: { username: 'postmaster@example.com', password: env.SMTP_PW },\n\tfrom: 'postmaster@example.com',\n\tto: ['ops@example.com'],\n\tsubject: 'Deploy finished',\n\ttext: 'The 14:00 deploy completed successfully.'\n});\n```\n\n### One-shot Connect + Send with HTML\n\n```typescript\nimport { connect } from 'edgeport/smtp';\n\nawait using smtp = await connect({\n\thostname: 'smtp.example.com',\n\tport: 465,\n\ttls: 'implicit',\n\tauth: { username: 'u', password: env.SMTP_PW }\n});\nawait smtp.send({ from: 'u@x', to: 'a@y', subject: 'Hi', html: '\u003ch1\u003eHi\u003c/h1\u003e', text: 'Hi' });\n```\n\n### Cloudflare Email Service\n\nUsing the [Cloudflare Email Service](https://developers.cloudflare.com/email-service/api/send-emails/smtp/), your workers can now send emails on-demand without the additional cost of a SMTP provider. For example:\n\n```typescript\nimport { send } from 'edgeport/smtp';\n\nawait send({\n\thostname: 'smtp.mx.cloudflare.net',\n\ttls: 'implicit', // port defaults to 465 for implicit\n\tauth: {\n\t\tusername: 'api_token', // literal string, per Cloudflare docs\n\t\tpassword: env.CF_EMAIL_TOKEN, // API token w/ Email Sending: Edit\n\t\tmechanism: 'PLAIN' // LOGIN also supported\n\t},\n\tfrom: 'welcome@yourdomain.com', // domain must be onboarded to Email Sending\n\tto: 'user@example.com', // any valid email address rather than verified recipient\n\tsubject: 'Hello',\n\ttext: 'Sent from a Worker over edgeport SMTP.'\n});\n```\n\n### Attachments and Plaintext Relays\n\nPass `attachments` to build a `multipart/mixed` message; pass `tls: 'off'` to talk to a\ntrusted internal relay or dev server with no TLS.\n\n```typescript\nawait send({\n\thostname: 'relay.internal',\n\ttls: 'off', // plaintext (trusted network); 'starttls' (default) and 'implicit' also supported\n\tfrom: 'reports@internal',\n\tto: 'team@internal',\n\tsubject: 'Daily report',\n\ttext: 'Attached.',\n\tattachments: [{ filename: 'report.csv', content: csvBytes, contentType: 'text/csv' }]\n});\n```\n\n## IMAP\n\n```typescript\nimport { connect, fetchRecent } from 'edgeport/imap';\n\nconst recent = await fetchRecent({\n\thostname: 'imap.example.com',\n\tauth: { username: 'u', password: env.PW },\n\tmailbox: 'INBOX',\n\tcount: 10\n});\n\nawait using imap = await connect({\n\thostname: 'imap.example.com',\n\tauth: { username: 'u', password: env.PW }\n});\nconst { exists } = await imap.select('INBOX');\nconst uids = await imap.search({ unseen: true });\nconst messages = await imap.fetch(uids, { envelope: true, body: true });\n```\n\n### One-shot Fetch Recent\n\nFetch the most recent N messages in one call (not streaming):\n\n```typescript\nimport { fetchRecent } from 'edgeport/imap';\nconst recent = await fetchRecent({\n\thostname: 'imap.example.com',\n\tauth: { username: 'u', password: env.PW },\n\tmailbox: 'INBOX',\n\tcount: 10\n});\n```\n\n## POP3\n\n```typescript\nimport { retrieveAll, connect } from 'edgeport/pop3';\n\nconst all = await retrieveAll({\n\thostname: 'pop.example.com',\n\tauth: { username: 'u', password: env.PW }\n});\n\nawait using pop = await connect({\n\thostname: 'pop.example.com',\n\tauth: { username: 'u', password: env.PW }\n});\nconst { count } = await pop.stat();\nconst first = await pop.retrieve(1);\n```\n\n### One-shot Retrieve All\n\nRetrieve all messages in one call (not streaming):\n\n```typescript\nimport { retrieveAll } from 'edgeport/pop3';\nconst all = await retrieveAll({\n\thostname: 'pop.example.com',\n\tauth: { username: 'u', password: env.PW }\n});\n```\n\n## WebSocket\n\nThe WebSocket client uses the platform WebSocket API; the runtime handles TLS, framing,\nand masking.\n\n```typescript\nimport { connect } from 'edgeport/ws';\n\nconst ws = await connect('wss://stream.example.com/feed', { protocols: ['v1'] });\nws.send(JSON.stringify({ subscribe: 'ticks' }));\n\n// directly iterate messages with `for await`\nfor await (const msg of ws) {\n\tif (msg.type === 'text') {\n\t\tconst event = JSON.parse(msg.data);\n\t\t// handle the parsed event here\n\t}\n}\nconst { code, reason } = await ws.closed;\n```\n\n### JSON Helpers\n\n```typescript\nimport { connect } from 'edgeport/ws';\n\nconst ws = await connect('wss://stream.example.com/feed');\nws.sendJson({ subscribe: 'ticks' });\nfor await (const msg of ws) {\n\tif (msg.type === 'text') {\n\t\tconst event = msg.json\u003c{ price: number }\u003e();\n\t\t// handle the parsed event here\n\t}\n}\n```\n\n## NATS\n\n```typescript\nimport { connect } from 'edgeport/nats';\n\nawait using nc = await connect({ hostname: 'nats.example.com', token: env.NATS_TOKEN });\n\n// pub/sub\nconst sub = nc.subscribe('orders.*', { queue: 'workers' });\nawait nc.publish('orders.created', JSON.stringify({ id: 42 }));\nfor await (const msg of sub) {\n\tconst order = JSON.parse(new TextDecoder().decode(msg.data));\n\t// handle the order here\n}\n\n// request-reply\nconst reply = await nc.request('time.now', '', { timeoutMs: 1000 });\n```\n\n### NKey Authorization\n\nnkey (ed25519) auth: pass `nkeySeed` (a `SU...` seed) instead of a token. For managed NATS\n(Synadia NGS) or any JWT-secured deployment, pass the contents of a `.creds` file - the user\nJWT and signing seed are extracted automatically:\n\n```typescript\nimport { connect } from 'edgeport/nats';\n\nawait using nc = await connect({ hostname: 'connect.ngs.global', creds: env.NATS_CREDS });\n```\n\n### Subscribe with a Queue Group\n\nDistribute messages across multiple workers by subscribing with a queue group:\n\n```typescript\nimport { connect } from 'edgeport/nats';\n\nawait using nc = await connect({ hostname: 'nats.example.com', token: env.NATS_TOKEN });\nconst sub = nc.subscribe('orders.*', { queue: 'workers' });\n\n// directly iterate messages with `for await`\nfor await (const msg of sub) {\n\tconst order = JSON.parse(new TextDecoder().decode(msg.data));\n\t// one member of the 'workers' queue group receives each message\n}\n```\n\n### JetStream\n\nDurable, at-least-once streams via `nc.jetstream()`: ensure a stream, publish with a\n`PubAck`, and pull with a durable consumer that survives reconnects (un-acked messages are\nredelivered; acked ones are not).\n\n```typescript\nawait using nc = await connect({ hostname: 'nats.example.com', token: env.NATS_TOKEN });\nconst js = nc.jetstream();\n\nawait js.ensureStream('EVENTS', { subjects: ['events.\u003e'] });\nconst ack = await js.publish('events.created', JSON.stringify({ id: 42 })); // { stream, seq }\n\nconst consumer = await js.pullSubscribe('EVENTS', 'worker-durable', { ackWaitMs: 30_000 });\nfor (const msg of await consumer.fetch(10, { expiresMs: 5000 })) {\n\t// process msg.data here\n\tawait msg.ack(); // un-acked messages are redelivered after a reconnect\n}\n```\n\n### JSON Helpers\n\n`publishJson`/`requestJson` and the per-message `json()`/`text()` accessors remove the\nencode/parse boilerplate:\n\n```typescript\nimport { connect } from 'edgeport/nats';\n\nawait using nc = await connect({ hostname: 'nats.example.com', token: env.NATS_TOKEN });\n\nawait nc.publishJson('readings', { temp: 21.5 });\nfor await (const { value } of nc.subscribeJson\u003c{ temp: number }\u003e('readings')) {\n\tconsole.log(value.temp);\n}\n\nconst reply = await nc.requestJson\u003c{ sum: number }\u003e('calc.add', [2, 3]);\n```\n\nA subscribe responder replies with `msg.respond(value)`; any received message exposes\n`msg.json\u003cT\u003e()` and `msg.text()`.\n\n## MQTT\n\nRaw TCP (1883 / TLS 8883) or over WebSocket - same API.\n\n```typescript\nimport { connect, connectWebSocket } from 'edgeport/mqtt';\n\nawait using mqtt = await connect({\n\thostname: 'broker.example.com',\n\tport: 8883,\n\ttls: 'implicit',\n\tclientId: 'edge-worker-1',\n\tusername: env.MQTT_USER,\n\tpassword: env.MQTT_PASS\n});\n\nawait mqtt.publish('sensors/edge/temp', '21.4', { qos: 1, retain: true });\nconst sub = mqtt.subscribe('sensors/+/temp', { qos: 1 });\nfor await (const m of sub) console.log(m.topic, new TextDecoder().decode(m.payload));\n\n// or tunnel MQTT through a WebSocket broker endpoint\nconst overWs = await connectWebSocket('wss://broker.example.com:8884/mqtt', {\n\tclientId: 'edge-ws'\n});\n```\n\n### Subscribe with Wildcards\n\nSubscribe to topics with `+` (single-level) and `#` (multi-level) wildcards:\n\n```typescript\nimport { connect } from 'edgeport/mqtt';\n\nawait using mqtt = await connect({ hostname: 'broker.example.com', clientId: 'edge' });\nconst sub = mqtt.subscribe('sensors/+/temp', { qos: 1 });\n\nfor await (const m of sub) {\n\tconsole.log(m.topic, new TextDecoder().decode(m.payload));\n}\n```\n\n### Last Will and Persistent Sessions\n\nSet a `will` and the broker publishes it if the client drops without a clean disconnect -\nthe basis for presence / offline detection. Use `cleanSession: false` with a fixed\n`clientId` so queued QoS-1 messages are drained on reconnect.\n\n```typescript\nawait using device = await connect({\n\thostname: 'broker.example.com',\n\tclientId: 'device-42',\n\tcleanSession: false, // persistent session: queued QoS\u003e=1 messages survive a reconnect\n\twill: { topic: 'devices/42/status', payload: 'offline', qos: 1, retain: true }\n});\nawait device.publish('devices/42/status', 'online', { retain: true });\n\n// ... later, an unexpected drop publishes the will; a clean shutdown does not:\nawait device.close({ graceful: false }); // abrupt -\u003e broker fires the 'offline' will\n```\n\n### JSON Helpers\n\n```typescript\nimport { connect } from 'edgeport/mqtt';\n\nawait using mqtt = await connect({ hostname: 'broker.example.com', clientId: 'edge' });\nawait mqtt.publishJson('sensors/1', { temp: 21.5 }, { qos: 1 });\nfor await (const { topic, value } of mqtt.subscribeJson\u003c{ temp: number }\u003e('sensors/+', {\n\tqos: 1\n})) {\n\tconsole.log(topic, value.temp);\n}\n```\n\nA plain `subscribe` message also exposes `msg.json\u003cT\u003e()` and `msg.text()`.\n\n## STOMP\n\n```typescript\nimport { connect } from 'edgeport/stomp';\n\nawait using stomp = await connect({\n\thostname: 'mq.example.com',\n\tlogin: env.MQ_USER,\n\tpasscode: env.MQ_PASS\n});\n\nawait stomp.send('/queue/jobs', JSON.stringify({ task: 'resize' }));\nconst sub = stomp.subscribe('/queue/jobs', { ack: 'client' });\nfor await (const m of sub) {\n\t// process the job (m.body) here\n\tawait m.ack?.();\n}\n```\n\n### Heartbeats\n\nCloud messaging brokers often require heartbeats to keep the connection alive. edgeport supports STOMP heartbeats:\n\n```typescript\nimport { connect } from 'edgeport/stomp';\n\nawait using stomp = await connect({\n\thostname: 'mq.example.com',\n\tlogin: env.MQ_USER,\n\tpasscode: env.MQ_PASS,\n\theartBeat: [10000, 10000] // [send, expect] in ms; negotiated down with the broker\n});\n```\n\n### Transactions\n\nStage sends in a transaction; the broker releases them only on `commit()` and discards them\non `abort()`.\n\n```typescript\nconst tx = await stomp.begin();\nawait tx.send('/queue/orders', JSON.stringify(order));\nawait tx.send('/queue/audit', JSON.stringify(entry));\nif (ok)\n\tawait tx.commit(); // both messages delivered atomically\nelse await tx.abort(); // neither is ever delivered\n```\n\n### JSON Helpers\n\n```typescript\nimport { connect } from 'edgeport/stomp';\n\nawait using stomp = await connect({\n\thostname: 'mq.example.com',\n\tlogin: env.MQ_USER,\n\tpasscode: env.MQ_PASS\n});\nawait stomp.sendJson('/queue/jobs', { job: 'reindex', n: 7 }); // sets content-type: application/json\nconst sub = stomp.subscribe('/queue/jobs');\nfor await (const msg of sub) {\n\tconst job = msg.json\u003c{ job: string; n: number }\u003e();\n\t// handle the job here\n}\n```\n\n## FTP\n\nPlaintext FTP, passive mode (Workers cannot accept the inbound connections active mode needs).\n\n```typescript\nimport { connect, getFile, putFile } from 'edgeport/ftp';\n\nawait using ftp = await connect({\n\thostname: 'files.example.com',\n\tusername: 'u',\n\tpassword: env.FTP_PW\n});\nawait ftp.put('reports/today.csv', new TextEncoder().encode('a,b,c\\n'));\nfor (const entry of await ftp.list('reports')) console.log(entry.name, entry.size);\nconst bytes = await ftp.get('reports/today.csv');\n```\n\n### ASCII Mode and Resume\n\n`get`/`put` take an options object: `type: 'ascii'` issues `TYPE A` (line-ending conversion)\nvs the default `'binary'` (`TYPE I`); `offset` issues `REST \u003cn\u003e` to resume a download, and\n`append: true` resumes an upload (`APPE`) - the practical recovery path after a dropped data\nchannel.\n\n```typescript\n// resume an interrupted upload from where it stopped, and download just the tail\nawait ftp.put('big.bin', firstChunk);\nawait ftp.put('big.bin', restOfFile, { append: true });\nconst tail = await ftp.get('big.bin', { offset: firstChunk.length });\n\n// transfer a text file in ASCII mode\nawait ftp.put('records.txt', data, { type: 'ascii' });\n```\n\n### Convenience Helpers\n\nHigher-level helpers over the raw commands; paths resolve relative to the working directory\nunless absolute.\n\n```typescript\nimport { connect } from 'edgeport/ftp';\n\nawait using ftp = await connect({\n\thostname: 'files.example.com',\n\tusername: 'u',\n\tpassword: env.FTP_PW\n});\n\nif (await ftp.exists('/etc/app/config.json')) {\n\tconst cfg = await ftp.getJson\u003c{ name: string }\u003e('/etc/app/config.json');\n}\n\nawait ftp.ensureDir('/incoming/2026/reports'); // recursive mkdir, one MKD per segment\nawait ftp.putText('/incoming/note.txt', 'hello world\\n');\nconst note = await ftp.getText('/incoming/note.txt');\nconst when = await ftp.mtime('/incoming/note.txt'); // MDTM, parsed as UTC\n\nawait ftp.removeAll('/incoming/2026'); // recursive, client-side, non-atomic; refuses '' and '/'\n```\n\n## LDAP / LDAPS\n\n```typescript\nimport { connect } from 'edgeport/ldap';\n// or: import { connect } from 'edgeport/ldaps'  // implicit TLS on 636\n\nawait using ldap = await connect({\n\thostname: 'ldap.example.com',\n\tbindDN: 'cn=svc,dc=example,dc=org',\n\tpassword: env.LDAP_PW\n});\n\nconst users = await ldap.search({\n\tbase: 'ou=people,dc=example,dc=org',\n\tscope: 'sub',\n\tfilter: '(\u0026(objectClass=person)(mail=*@example.org))',\n\tattributes: ['cn', 'mail']\n});\nfor (const u of users) console.log(u.dn, u.attributes.mail);\n```\n\n### StartTLS\n\nLDAP StartTLS is supported on the standard LDAP port (389):\n\n```typescript\nimport { connect } from 'edgeport/ldap';\n\nawait using ldap = await connect({\n\thostname: 'ldap.example.com',\n\tport: 389,\n\ttls: 'starttls',\n\tbindDN: 'cn=svc,dc=example,dc=org',\n\tpassword: env.LDAP_PW\n});\n\nconst entries = await ldap.search({\n\tbase: 'ou=people,dc=example,dc=org',\n\tscope: 'sub',\n\tfilter: '(uid=jdoe)',\n\tattributes: ['cn', 'mail']\n});\n```\n\n### LDAP Filters\n\nLDAP search filters are expressed in RFC 4515 syntax:\n\n```typescript\nimport { connect } from 'edgeport/ldap';\n\nawait using ldap = await connect({\n\thostname: 'ldap.example.com',\n\tbindDN: 'cn=svc,dc=example,dc=org',\n\tpassword: env.LDAP_PW\n});\n\nconst users = await ldap.search({\n\tbase: 'ou=people,dc=example,dc=org',\n\tscope: 'sub',\n\tfilter: '(\u0026(objectClass=person)(mail=*@example.org))',\n\tattributes: ['cn', 'mail']\n});\n```\n\n### Filter Builders\n\nBuild search filters from untrusted input safely. The structured builders carry values\nliterally, so a `*`, `(`, or `)` in user input becomes a literal byte on the wire and can\nnever inject filter syntax. Drop the result straight into `search({ filter })` / `findOne`.\n\n```typescript\nimport { connect, and, eq, present } from 'edgeport/ldap';\n\nawait using ldap = await connect({\n\thostname: 'ldap.example.com',\n\tbindDN: 'cn=svc,dc=example,dc=org',\n\tpassword: env.LDAP_PW\n});\n\nconst users = await ldap.search({\n\tbase: 'ou=people,dc=example,dc=org',\n\tfilter: and(eq('objectClass', 'person'), present('mail')) // (\u0026(objectClass=person)(mail=*))\n});\nconst one = await ldap.findOne({\n\tbase: 'ou=people,dc=example,dc=org',\n\tfilter: eq('uid', userInput)\n});\n```\n\nBuilders: `and`, `or`, `not`, `eq`, `present`, `gte`, `lte`, `approx`, `substring`, `contains`\n(also grouped under a `filters` namespace). When you assemble a filter string or DN by hand\ninstead, escape the interpolated value with `escapeFilterValue` / `escapeDN`.\n\n`authenticate` does the bind-search-bind verify flow in one call:\n\n```typescript\nimport { authenticate, eq } from 'edgeport/ldap';\n\nconst entry = await authenticate({\n\thostname: 'ldap.example.com',\n\tbindDN: 'cn=svc,dc=example,dc=org',\n\tbindPassword: env.SVC_PW,\n\tbase: 'ou=people,dc=example,dc=org',\n\tuserFilter: eq('uid', username),\n\tpassword: submittedPassword\n});\nif (entry) {\n\t// authenticated; entry.dn is the bound user\n}\n```\n\n## Syslog\n\n```typescript\nimport { connect, Severity } from 'edgeport/syslog';\n\nawait using log = await connect({\n\thostname: 'logs.example.com',\n\tport: 6514,\n\ttls: 'implicit',\n\tappName: 'edge-worker'\n});\nawait log.log({\n\tseverity: Severity.info,\n\tmessage: 'request handled',\n\tstructuredData: [{ id: 'req@1', params: { ms: '12' } }]\n});\n```\n\n### Severity Shortcuts\n\n`info`/`notice`/`warn`/`error`/`debug` delegate to `log()` with the matching severity:\n\n```typescript\nimport { connect } from 'edgeport/syslog';\n\nawait using log = await connect({ hostname: 'logs.example.com', appName: 'edge-worker' });\nawait log.info('request handled');\nawait log.warn('disk almost full', { facility: 'local0' });\nawait log.error('request failed', { structuredData: [{ id: 'req@1', params: { code: '500' } }] });\n```\n\n## Real-World Recipes\n\n\u003e [!NOTE]\n\u003e For larger **multi-protocol** workflows - mail automation, secure deploy/ops, an HL7\n\u003e integration engine, device fleet management, resilience/recovery, and more - see\n\u003e [ADVANCED_USAGE.md](./ADVANCED_USAGE.md), each backed by an end-to-end integration test in\n\u003e [`test/integration/recipes/`](./test/integration/recipes/).\n\n### A cron Worker that runs a Remote Command\n\n```typescript\nimport { exec } from 'edgeport/ssh';\n\nexport default {\n\tasync scheduled(_event, env) {\n\t\tconst { code } = await exec({\n\t\t\thostname: env.BOX,\n\t\t\tusername: 'deploy',\n\t\t\tprivateKey: { pem: env.SSH_KEY },\n\t\t\tcommand: 'systemctl restart myapp'\n\t\t});\n\t\tif (code !== 0) throw new Error('restart failed');\n\t}\n};\n```\n\n### Email an Alert on a Webhook\n\n```typescript\nimport { send } from 'edgeport/smtp';\n\nexport default {\n\tasync fetch(req, env) {\n\t\tconst body = await req.text();\n\t\tawait send({\n\t\t\thostname: env.SMTP_HOST,\n\t\t\tauth: { username: env.SMTP_USER, password: env.SMTP_PW },\n\t\t\tfrom: env.SMTP_USER,\n\t\t\tto: env.ONCALL,\n\t\t\tsubject: 'Webhook alert',\n\t\t\ttext: body\n\t\t});\n\t\treturn new Response('ok');\n\t}\n};\n```\n\n### Poll a Mailbox and Archive Attachments to SFTP\n\n```typescript\nimport { connect as imapConnect } from 'edgeport/imap';\nimport { putFile } from 'edgeport/sftp';\n\nawait using imap = await imapConnect({\n\thostname: env.IMAP,\n\tauth: { username: env.U, password: env.P }\n});\nawait imap.select('INBOX');\nconst uids = await imap.search({ unseen: true });\nconst messages = await imap.fetch(uids, { body: true });\nfor (const m of messages) {\n\tif (m.body)\n\t\tawait putFile({\n\t\t\thostname: env.SFTP,\n\t\t\tusername: env.U,\n\t\t\tpassword: env.P,\n\t\t\tpath: `/archive/${m.uid}.eml`,\n\t\t\tdata: m.body\n\t\t});\n}\n```\n\n### Run a Remote Command and Publish the Result to NATS\n\n```typescript\nimport { connect as sshConnect } from 'edgeport/ssh';\nimport { connect as natsConnect } from 'edgeport/nats';\n\nawait using ssh = await sshConnect({\n\thostname: env.BOX,\n\tusername: 'deploy',\n\tprivateKey: { pem: env.SSH_KEY }\n});\nconst disk = await ssh.df('/'); // parsed rows: { filesystem, sizeKb, usedKb, availKb, ... }\nawait using nc = await natsConnect({ hostname: env.NATS, token: env.NATS_TOKEN });\nawait nc.publishJson('telemetry.disk', disk); // JSON-encodes the structured usage\n```\n\n### Bridge MQTT Sensor Readings to Syslog (edge observability)\n\n```typescript\nimport { connect as mqttConnect } from 'edgeport/mqtt';\nimport { connect as syslogConnect, Severity } from 'edgeport/syslog';\n\nawait using mqtt = await mqttConnect({ hostname: env.BROKER, clientId: 'edge-bridge' });\nawait using log = await syslogConnect({\n\thostname: env.SIEM,\n\tport: 6514,\n\ttls: 'implicit',\n\tappName: 'sensors'\n});\n\nfor await (const reading of mqtt.subscribe('sensors/#', { qos: 1 })) {\n\tawait log.log({\n\t\tseverity: Severity.info,\n\t\tmessage: `${reading.topic}=${new TextDecoder().decode(reading.payload)}`\n\t});\n}\n```\n\n### Authorize an Action via LDAP, then Alert over STOMP\n\n```typescript\nimport { connect as ldapConnect } from 'edgeport/ldaps';\nimport { eq } from 'edgeport/ldap';\nimport { connect as stompConnect } from 'edgeport/stomp';\n\nawait using dir = await ldapConnect({\n\thostname: env.LDAP,\n\tbindDN: env.SVC_DN,\n\tpassword: env.SVC_PW\n});\n// eq() carries the value literally, so an untrusted uid can't inject filter syntax\nconst allowed = await dir.search({ base: 'ou=people,dc=example,dc=org', filter: eq('uid', uid) });\n\nif (allowed.length === 0) {\n\tawait using mq = await stompConnect({\n\t\thostname: env.MQ,\n\t\tlogin: env.MQ_USER,\n\t\tpasscode: env.MQ_PW\n\t});\n\tawait mq.send('/queue/security.alerts', `unauthorized action by ${uid}`);\n}\n```\n\n### Pull a File over FTP and Republish to MQTT\n\n```typescript\nimport { getFile } from 'edgeport/ftp';\nimport { connect as mqttConnect } from 'edgeport/mqtt';\n\nconst csv = await getFile({\n\thostname: env.FTP,\n\tusername: env.U,\n\tpassword: env.P,\n\tpath: '/exports/prices.csv'\n});\nawait using mqtt = await mqttConnect({ hostname: env.BROKER, clientId: 'price-feed' });\nawait mqtt.publish('feeds/prices', csv, { qos: 1, retain: true });\n```\n\n## Error Handling\n\nEvery edgeport call rejects with one of four types, all extending `EdgeportError`:\n\n```typescript\nimport { AuthError, ConnectionError, ProtocolError, TimeoutError } from 'edgeport';\n\ntry {\n  await exec({ hostname: 'h', username: 'u', password: 'wrong', command: 'id' });\n} catch (err) {\n  if (err instanceof AuthError) // bad credentials\n  else if (err instanceof ConnectionError) // could not reach / TLS / dropped\n  else if (err instanceof ProtocolError) // malformed or unsupported (e.g. no common cipher)\n  else if (err instanceof TimeoutError) // a deadline elapsed\n}\n```\n\n## Limitations\n\n- **No port 25.** Cloudflare blocks outbound port 25; use 587 or 465 for SMTP.\n- **ChaCha20 throughput.** `chacha20-poly1305@openssh.com` is pure-JS (via `@noble/ciphers`)\n  and slower than the hardware-paced AES-GCM that WebCrypto provides; AES-GCM is preferred\n  during negotiation. Watch Worker CPU limits on very large ChaCha transfers.\n- **No agent forwarding or X11.** Local port forwarding is supported via `forwardOut`\n  (`direct-tcpip` reach-through); SOCKS (`-D`) and remote (`-R`) forwarding are out of scope\n  because Workers cannot accept inbound connections.\n- **Encrypted private keys**: encrypted PKCS#8 (PBES2) and OpenSSH-format keys are\n  supported; legacy `DEK-Info` PEM and `aes-gcm`/`chacha20-poly1305` OpenSSH key ciphers\n  are not (re-encrypt with `aes256-ctr`).\n- **Time-based rekey** is not triggered (byte-volume and server-initiated rekeys are).\n- **FTPS is not provided.** The Workers `startTls` API exposes no TLS session export/import,\n  so the FTPS data connection cannot resume the control channel's TLS session - which strict\n  servers (e.g. vsftpd `require_ssl_reuse=YES`) mandate. Shipping it would only work against\n  servers that disable that protection, so plain `edgeport/ftp` is provided and FTPS is\n  deferred until the runtime gains TLS session control.\n- **FTP is passive-mode only** (Workers cannot accept the inbound connections active mode needs).\n- **LDAP SASL** is not implemented (simple bind over TLS is). The one mechanism that would add\n  capability over simple bind, SASL EXTERNAL, needs a TLS client certificate, which the Workers\n  socket API cannot present; PLAIN is equivalent to simple bind, and SCRAM/DIGEST/GSSAPI are\n  rarely required. NATS auth is fully covered (token / user-pass / nkey / JWT); NATS does not\n  use SASL. MQTT/STOMP/Syslog authenticate via TLS + username-password.\n\n## Advanced: Building-Block Exports\n\nBeyond the protocol modules, edgeport publishes the lower-level SSH building blocks it is\nassembled from, for tooling that needs them directly:\n\n| Import            | Provides                                                                                                                         |\n| ----------------- | -------------------------------------------------------------------------------------------------------------------------------- |\n| `edgeport/wire`   | SSH binary wire codecs (`SshReader`, `SshWriter`, `toMpintBody`)                                                                 |\n| `edgeport/crypto` | hashes/HMAC, the SSH packet ciphers (`createPacketCipher`, `cipherSizes`), host/user keys (`verifyHostSignature`, `loadUserKey`) |\n| `edgeport/kex`    | KEXINIT negotiation, the exchange hash + key schedule, `createKex`, and the `curve25519`/`nistp256` namespaces                   |\n| `edgeport/auth`   | SSH user authentication (`authenticate`)                                                                                         |\n\n```typescript\nimport { SshReader, SshWriter } from 'edgeport/wire';\nimport { createPacketCipher, verifyHostSignature } from 'edgeport/crypto';\nimport { negotiate, createKex } from 'edgeport/kex';\n```\n\nThese are stable but lower-level; most applications only need the protocol modules above.\n\n## API Reference\n\nFull generated API docs: see the published TypeDoc site (built from source by\n`bun run docs:build`).\n\n## Contributing\n\n```sh\nbun install\nbun run typecheck\nbun run test                                   # gate: unit + KAT, hermetic\ndocker compose -f docker/compose.yml up -d --wait\nINTEGRATION=1 bun run test                     # integration under workerd vs real servers\ndocker compose -f docker/compose.yml down -v\n```\n\n## License\n\nMIT (c) Gregory Mitchell 2026. See [LICENSE](./LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgmitch215%2Fedgeport","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgmitch215%2Fedgeport","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgmitch215%2Fedgeport/lists"}