{"_id":"@avelor/bifrost","_rev":"5-9d591fcd88c4524efa3e33e2406d8f4e","name":"@avelor/bifrost","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@avelor/bifrost","version":"0.1.0","keywords":["tunnel","relay","websocket","ngrok","self-hosted","proxy"],"author":{"name":"Avelor","email":"cruz@avelor.es"},"license":"MIT","_id":"@avelor/bifrost@0.1.0","maintainers":[{"name":"christianecg","email":"contacto@christianecg.com"}],"homepage":"https://github.com/avelor-es/bifrost#readme","bugs":{"url":"https://github.com/avelor-es/bifrost/issues"},"bin":{"bifrost":"bin/bifrost.js"},"dist":{"shasum":"81de90b7e5f2f1cd66e2a6d509f78d87f5c254df","tarball":"https://registry.npmjs.org/@avelor/bifrost/-/bifrost-0.1.0.tgz","fileCount":12,"integrity":"sha512-UzB9zx8S9tqM7aDDh3TnhJMit74eRgH/44/E1GCcWH8MRaBOM49Aadz8m2uLohJ6PGAq1dDOHd1QKKRMihGrcg==","signatures":[{"sig":"MEUCIA6xr5Zfj+gQMW5bHEKccDns4ctGsSCtSWkpo4s8LaiIAiEAy5cpkS0wMm9uSWv2h+DbTiWWBx5lCqf+wEPjJJao+gA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":36247},"main":"src/relay.js","engines":{"node":">=18"},"gitHead":"d5c57dcd1f20480800b53fbdac99e67cee8beced","scripts":{"test":"node --test test/tokens.test.js test/config.test.js test/relay.test.js test/cli.test.js","start":"node bin/bifrost.js"},"_npmUser":{"name":"christianecg","email":"contacto@christianecg.com"},"repository":{"url":"git+https://github.com/avelor-es/bifrost.git","type":"git"},"_npmVersion":"11.6.2","description":"WebSocket tunnel relay — serve from a server, connect from anywhere","directories":{},"_nodeVersion":"24.13.0","dependencies":{"ws":"^8.18.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/bifrost_0.1.0_1780518292046_0.42264643462442075","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@avelor/bifrost","version":"0.1.1","keywords":["tunnel","relay","websocket","ngrok","self-hosted","proxy"],"author":{"name":"Avelor","email":"cruz@avelor.es"},"license":"MIT","_id":"@avelor/bifrost@0.1.1","maintainers":[{"name":"christianecg","email":"contacto@christianecg.com"}],"homepage":"https://github.com/avelor-es/bifrost#readme","bugs":{"url":"https://github.com/avelor-es/bifrost/issues"},"bin":{"bifrost":"bin/bifrost.js"},"dist":{"shasum":"a0c52690adb1259a8e03fe103dc926052ac6679a","tarball":"https://registry.npmjs.org/@avelor/bifrost/-/bifrost-0.1.1.tgz","fileCount":12,"integrity":"sha512-E8cSYcIGKDJJjSunWpqtUb+kzgxQtAMzY2pDzNrUAHX5WcTregKSrtETJH4cwhrF5gdovwd6AqGHem9zMOeavA==","signatures":[{"sig":"MEUCIQCwlvTmmohzdZgL+Hjej8mdMetZuSgIbCbkY6JFXLORAgIgMzMfG/izLlCGGme3EXSyseHaIaZNWkZP3Aq8PF2Ix1o=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":36750},"main":"src/relay.js","engines":{"node":">=18"},"gitHead":"d6e99d83dd37b5997dd7e39bbe6283f3ebbf939a","scripts":{"test":"node --test test/tokens.test.js test/config.test.js test/relay.test.js test/cli.test.js","start":"node bin/bifrost.js"},"_npmUser":{"name":"christianecg","email":"contacto@christianecg.com"},"repository":{"url":"git+https://github.com/avelor-es/bifrost.git","type":"git"},"_npmVersion":"11.6.2","description":"WebSocket tunnel relay — serve from a server, connect from anywhere","directories":{},"_nodeVersion":"24.13.0","dependencies":{"ws":"^8.18.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/bifrost_0.1.1_1780521398472_0.3133275538448519","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@avelor/bifrost","version":"0.1.2","description":"WebSocket tunnel relay — serve from a server, connect from anywhere","main":"src/relay.js","bin":{"bifrost":"bin/bifrost.js"},"scripts":{"start":"node bin/bifrost.js","test":"node --test test/tokens.test.js test/config.test.js test/relay.test.js test/cli.test.js"},"dependencies":{"ws":"^8.18.0"},"engines":{"node":">=18"},"license":"MIT","author":{"name":"Avelor","email":"cruz@avelor.es"},"publishConfig":{"access":"public"},"keywords":["tunnel","relay","websocket","ngrok","self-hosted","proxy"],"repository":{"type":"git","url":"git+https://github.com/avelor-es/bifrost.git"},"homepage":"https://github.com/avelor-es/bifrost#readme","bugs":{"url":"https://github.com/avelor-es/bifrost/issues"},"gitHead":"4a77da1e67e3ec599ce0ba1a37eea9f2dfdd81cb","_id":"@avelor/bifrost@0.1.2","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-yzKWIHNVcMWbstMR0n8ELuoSjUsj2Ngs7iWpB3d3uf2638JYqBZpc75WCFKvOo/TIpIhmgxPwArOl76CAk2JHw==","shasum":"c9fe2ec94993e9c34fd8e0150ef99dd383edd0af","tarball":"https://registry.npmjs.org/@avelor/bifrost/-/bifrost-0.1.2.tgz","fileCount":12,"unpackedSize":36955,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQClYufNPhptyyf610nlmapTSlfPjU8WuuydG/qdaqTfigIgEjOyUSE4xJ3oRCieBpHzwpttvRovzF8b0THhqNd8b1I="}]},"_npmUser":{"name":"christianecg","email":"contacto@christianecg.com"},"directories":{},"maintainers":[{"name":"christianecg","email":"contacto@christianecg.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/bifrost_0.1.2_1781054381881_0.4361999049891965"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-03T20:24:51.801Z","modified":"2026-06-10T01:19:42.146Z","1.0.0":"2026-06-03T20:17:53.735Z","0.1.0":"2026-06-03T20:24:52.204Z","0.1.1":"2026-06-03T21:16:38.621Z","0.1.2":"2026-06-10T01:19:42.036Z"},"bugs":{"url":"https://github.com/avelor-es/bifrost/issues"},"author":{"name":"Avelor","email":"cruz@avelor.es"},"license":"MIT","homepage":"https://github.com/avelor-es/bifrost#readme","keywords":["tunnel","relay","websocket","ngrok","self-hosted","proxy"],"repository":{"type":"git","url":"git+https://github.com/avelor-es/bifrost.git"},"description":"WebSocket tunnel relay — serve from a server, connect from anywhere","maintainers":[{"name":"christianecg","email":"contacto@christianecg.com"}],"readme":"# @avelor/bifrost\n\nSelf-hosted WebSocket tunnel relay. Expose a local port through your own server — no third-party services, no SSH, no cloud accounts.\n\n```\nInternet ──► your server (tunnel.example.com:443)\n                │\n                ▼  (reverse proxy to 127.0.0.1)\n         bifrost serve :9001\n                │\n                │  WebSocket (Bearer token)\n                │\n         bifrost connect (your machine)\n                │\n                │  HTTP\n                │\n         localhost:3000 (your dev server)\n```\n\nSingle dependency (`ws`). Requires Node.js 18+.\n\n![bifrost demo](demo.gif)\n\n## Installation\n\n```bash\nnpm install -g @avelor/bifrost\n```\n\n## Quick start\n\n**On your server:**\n\n```bash\n# Issue a token\nbifrost token issue --global\n#\n#   id:     a1b2c3d4\n#   scope:  global\n#   token:  bf_...\n#\n#   → run on your machine:\n#   bifrost use <endpoint> bf_...\n\n# Start the relay\nbifrost serve --port 9001 --host 127.0.0.1 --daemon\n```\n\n**On your machine:**\n\n```bash\nbifrost use wss://tunnel.example.com bf_...\nbifrost connect 3000\n```\n\nYour local port 3000 is now reachable at `tunnel.example.com`.\n\n## Commands\n\n### Server\n\n| Command | Description |\n|---|---|\n| `bifrost serve [--port N] [--host H] [--daemon]` | Start the relay |\n| `bifrost stop` | Stop the daemon |\n| `bifrost status` | Daemon status |\n\n`--host` defaults to `0.0.0.0`. When running behind a reverse proxy, pass `--host 127.0.0.1` to prevent direct access to the relay port.\n\n### Tokens\n\n| Command | Description |\n|---|---|\n| `bifrost token issue --scope <name>` | Token scoped to one subdomain |\n| `bifrost token issue --global` | Token valid for all subdomains |\n| `bifrost token list` | List active tokens (no raw values) |\n| `bifrost token revoke <id>` | Revoke a token by id |\n\n`bifrost token issue` without a flag is an error — the scope must be explicit.\n\n### Client\n\n| Command | Description |\n|---|---|\n| `bifrost use <endpoint> <token>` | Save endpoint and token to `~/.config/bifrost/` |\n| `bifrost connect <port>` | Expose localhost:<port> through the relay |\n| `bifrost connect <port> --name <name>` | Use a specific subdomain |\n| `bifrost connect <port> --run \"cmd\"` | Start a process and tunnel it |\n\n## Deploying the relay\n\n### Reverse proxy — nginx\n\n```nginx\nserver {\n    listen 443 ssl;\n    server_name tunnel.example.com *.tunnel.example.com;\n\n    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;\n\n    location / {\n        proxy_pass         http://127.0.0.1:9001;\n        proxy_http_version 1.1;\n        proxy_set_header   Upgrade    $http_upgrade;\n        proxy_set_header   Connection \"upgrade\";\n        proxy_set_header   Host       $host;\n        proxy_set_header   X-Real-IP  $remote_addr;\n        proxy_read_timeout 3600s;\n    }\n}\n```\n\n> The `*.tunnel.example.com` wildcard requires a wildcard TLS certificate (Let's Encrypt DNS challenge).\n\n### Reverse proxy — Apache\n\n```apache\n<VirtualHost *:443>\n    ServerName tunnel.example.com\n\n    SSLEngine on\n    SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem\n    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem\n\n    RewriteEngine On\n    RewriteCond %{HTTP:Upgrade} websocket [NC]\n    RewriteCond %{HTTP:Connection} upgrade [NC]\n    RewriteRule ^/_bifrost ws://127.0.0.1:9001/_bifrost [P,L]\n\n    ProxyPass        / http://127.0.0.1:9001/\n    ProxyPassReverse / http://127.0.0.1:9001/\n    ProxyPreserveHost On\n</VirtualHost>\n```\n\n### systemd service\n\n```ini\n# /etc/systemd/system/bifrost.service\n[Unit]\nDescription=Bifrost Relay\nAfter=network.target\n\n[Service]\nType=simple\nUser=www-data\nExecStart=bifrost serve --port 9001 --host 127.0.0.1\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=multi-user.target\n```\n\n```bash\nsystemctl enable --now bifrost\ncurl https://tunnel.example.com/_bifrost/ping  # → ok\n```\n\n## Token management\n\nTokens are stored in `~/.config/bifrost/tokens.json` on the server. Raw values are shown once at issuance and never stored — only the SHA-256 hash is kept.\n\n### Subdomain routing\n\nA token issued with `--scope preview` can only connect as `preview.tunnel.example.com`. A global token can connect under any name. The scope is validated on every WebSocket handshake.\n\n```bash\nbifrost connect 3000 --name preview\n# → tunnel active → https://preview.tunnel.example.com\n```\n\nYour reverse proxy and DNS must route `*.tunnel.example.com` to the relay for named tunnels to work.\n\n> **Domain naming requirement:** the relay extracts the tunnel name from the `Host` header by matching the pattern `<name>.tunnel.<tld>`. The literal segment `.tunnel.` must appear in your domain. `preview.tunnel.example.com` works; `preview.relay.example.com` or `preview.example.com` do not — the relay would treat those as the default tunnel instead of routing by name.\n\n## Client reconnection\n\nThe client reconnects automatically on disconnect with exponential backoff:\n\n```\n1s → 2s → 4s → 8s → 16s → 30s (cap)\n```\n\nRetries reset to zero on each successful connection. Reconnection stops only if the server explicitly rejects the handshake (wrong token or scope mismatch — WebSocket close code `1008`).\n\n## Config files\n\nAll files live under `~/.config/bifrost/`:\n\n| File | Purpose |\n|------|---------|\n| `config.json` | Client config: `endpoint` and `token` |\n| `tokens.json` | Server token store (hashed) |\n| `bifrost.pid` | Daemon PID |\n| `bifrost.log` | Daemon stdout/stderr |\n\n## How it works\n\n1. The relay starts an HTTP + WebSocket server on the configured port.\n2. The client connects to `/_bifrost` (or `/_bifrost/<name>`) via WebSocket with a Bearer token.\n3. The relay validates the token and its scope against the requested tunnel name.\n4. Incoming HTTP requests are serialized (method, URL, headers, base64 body) and forwarded to the client over the WebSocket.\n5. The client forwards them to `localhost:<port>` and sends the response back.\n6. The relay writes the response to the original HTTP caller.\n\n## Security\n\n- Tokens are SHA-256 hashed at rest. Raw values are never logged or stored.\n- Scoped tokens are validated against the tunnel name on every handshake — a `preview` token cannot connect as `staging`.\n- The relay pings every connected client every 30 seconds. Clients that do not respond are terminated, preventing ghost entries from holding memory or blocking a name.\n- The pending request queue is capped at 256 entries. Excess requests receive `429 Too Many Requests` immediately.\n- Run the relay with `--host 127.0.0.1` behind a reverse proxy so it is not directly reachable from the internet.\n\n## License\n\nMIT © Avelor\n","readmeFilename":"README.md"}