Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

@rubriclab/bunl

Package Overview
Dependencies
Maintainers
4
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@rubriclab/bunl - npm Package Compare versions

Comparing version
0.2.0
to
0.2.1
+3
CHANGELOG.md
- [2026-02-06] [Bump version](https://github.com/rubriclab/bunl/commit/0043caed41428d060517223cbb8d72fe52813692)
# Changelog
import { serve } from 'bun'
import { renderToStaticMarkup } from 'react-dom/server'
const port = Number(Bun.env.DEMO_PORT) || 3000
const PNG_1PX = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
'base64'
)
const FAKE_FONT = (() => {
const buf = Buffer.alloc(256)
for (let i = 0; i < 256; i++) buf[i] = i
return buf
})()
const css = `* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #fff;
color: #000;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
main {
max-width: 480px;
width: 100%;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.02em;
margin-bottom: 1rem;
}
p {
color: #666;
margin-bottom: 1.5rem;
line-height: 1.6;
}
img {
display: block;
margin-bottom: 1.5rem;
border: 1px solid #eee;
}
nav a {
color: #000;
text-decoration: none;
border-bottom: 1px solid #ccc;
}
nav a:hover {
border-color: #000;
}
nav span {
color: #ccc;
margin: 0 0.5rem;
}`
function Page() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>bunl</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<main>
<h1>bunl</h1>
<p>Served through a tunnel.</p>
<img src="/image.png" alt="test" width={100} height={100} />
<nav>
<a href="/api/health">API</a>
<span>·</span>
<a href="/font.woff2">Font</a>
</nav>
</main>
</body>
</html>
)
}
const html = `<!DOCTYPE html>${renderToStaticMarkup(<Page />)}`
serve({
fetch(req) {
const { pathname } = new URL(req.url)
if (pathname === '/style.css')
return new Response(css, { headers: { 'content-type': 'text/css; charset=utf-8' } })
if (pathname === '/image.png')
return new Response(new Uint8Array(PNG_1PX), { headers: { 'content-type': 'image/png' } })
if (pathname === '/font.woff2')
return new Response(new Uint8Array(FAKE_FONT), { headers: { 'content-type': 'font/woff2' } })
if (pathname === '/api/health') return Response.json({ status: 'ok', timestamp: Date.now() })
if (pathname === '/echo' && req.method === 'POST')
return new Response(req.body, {
headers: {
'content-type': req.headers.get('content-type') || 'application/octet-stream'
}
})
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } })
},
port
})
console.log(`Demo server at http://localhost:${port}`)
+43
-4

@@ -22,4 +22,42 @@ #! /usr/bin/env bun

}
function page(title, body) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${title} \u2014 bunl</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #fff;
color: #000;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
main { max-width: 480px; width: 100%; }
h1 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 1rem; }
p { color: #666; line-height: 1.6; margin-bottom: 0.75rem; }
code { background: #f5f5f5; padding: 0.15em 0.4em; border-radius: 3px; font-size: 0.9em; }
a { color: #000; }
</style>
</head>
<body>
<main>
${body}
</main>
</body>
</html>`;
}
// client.ts
function badGatewayHtml(port) {
return page("Bad Gateway", `<h1>Bad Gateway</h1>
<p>Could not reach <code>localhost:${port}</code>.</p>
<p>Make sure your local server is running.</p>`);
}
async function main({

@@ -84,5 +122,6 @@ port,

console.error(`\x1B[31mERR\x1B[0m ${req.method} ${req.pathname}: ${err}`);
const html = badGatewayHtml(port || "3000");
const response = {
body: toBase64(new TextEncoder().encode(`Failed to reach localhost:${port} \u2014 ${err}`).buffer),
headers: { "content-type": "text/plain" },
body: toBase64(new TextEncoder().encode(html).buffer),
headers: { "content-type": "text/html; charset=utf-8" },
id: req.id,

@@ -113,3 +152,3 @@ status: 502,

options: {
domain: { default: "localhost:1234", short: "d", type: "string" },
domain: { default: "bunl.sh", short: "d", type: "string" },
open: { short: "o", type: "boolean" },

@@ -127,3 +166,3 @@ port: { default: "3000", short: "p", type: "string" },

main({
domain: values.domain || "localhost:1234",
domain: values.domain || "bunl.sh",
open: values.open || false,

@@ -130,0 +169,0 @@ port: values.port || "3000",

@@ -9,2 +9,4 @@ {

"@rubriclab/config": "^0.0.24",
"react": "^19.2.4",
"react-dom": "^19.2.4",
},

@@ -14,2 +16,4 @@ "devDependencies": {

"@types/bun": "latest",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
},

@@ -89,4 +93,10 @@ },

"@types/react": ["@types/react@19.2.13", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],

@@ -132,2 +142,8 @@

"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],

@@ -134,0 +150,0 @@

+15
-12
import { parseArgs } from 'node:util'
import type { TunnelInit, TunnelRequest, TunnelResponse } from './types'
import { fromBase64, openBrowser, toBase64 } from './utils'
import { fromBase64, openBrowser, page, toBase64 } from './utils'
function badGatewayHtml(port: string) {
return page(
'Bad Gateway',
`<h1>Bad Gateway</h1>
<p>Could not reach <code>localhost:${port}</code>.</p>
<p>Make sure your local server is running.</p>`
)
}
async function main({

@@ -21,4 +30,2 @@ port,

// Auto-detect ws:// vs wss:// — use secure WebSocket for anything
// that isn't localhost or an explicit IP
const isLocal = /^(localhost|127\.|0\.0\.0\.0|\[::1\])/.test(domain || '')

@@ -34,3 +41,2 @@ const wsScheme = isLocal ? 'ws' : 'wss'

// Initial connection — server tells us our public URL
if (data.type === 'init') {

@@ -43,3 +49,2 @@ const init = data as TunnelInit

// Incoming tunnel request — proxy to local server
if (data.type === 'request') {

@@ -50,7 +55,5 @@ const req = data as TunnelRequest

try {
// Decode base64 request body
const reqBody =
req.body && req.method !== 'GET' && req.method !== 'HEAD' ? fromBase64(req.body) : null
// Remove headers that would conflict with the local fetch
const fwdHeaders = { ...req.headers }

@@ -70,3 +73,2 @@ delete fwdHeaders.host

// Read response as binary, encode to base64
const resBody = await res.arrayBuffer()

@@ -91,5 +93,6 @@ const headers: Record<string, string> = {}

const html = badGatewayHtml(port || '3000')
const response: TunnelResponse = {
body: toBase64(new TextEncoder().encode(`Failed to reach localhost:${port} — ${err}`).buffer),
headers: { 'content-type': 'text/plain' },
body: toBase64(new TextEncoder().encode(html).buffer),
headers: { 'content-type': 'text/html; charset=utf-8' },
id: req.id,

@@ -124,3 +127,3 @@ status: 502,

options: {
domain: { default: 'localhost:1234', short: 'd', type: 'string' },
domain: { default: 'bunl.sh', short: 'd', type: 'string' },
open: { short: 'o', type: 'boolean' },

@@ -140,3 +143,3 @@ port: { default: '3000', short: 'p', type: 'string' },

main({
domain: values.domain || 'localhost:1234',
domain: values.domain || 'bunl.sh',
open: values.open || false,

@@ -143,0 +146,0 @@ port: values.port || '3000',

@@ -8,7 +8,2 @@ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'

/**
* Make a request through the tunnel using Host-header routing.
* This avoids needing wildcard DNS (*.localhost) to resolve,
* which doesn't work in all environments.
*/
function tunnelFetch(path: string, init?: RequestInit): Promise<Response> {

@@ -27,3 +22,2 @@ const headers = new Headers(init?.headers as HeadersInit)

/** Wait until a URL responds (or timeout). */
async function waitFor(url: string, timeoutMs = 10_000): Promise<void> {

@@ -42,3 +36,2 @@ const start = Date.now()

/** Wait until a tunnel subdomain is connected (server returns non-404). */
async function waitForTunnel(timeoutMs = 15_000): Promise<void> {

@@ -51,3 +44,3 @@ const start = Date.now()

} catch {
// not ready yet
/* not ready */
}

@@ -60,3 +53,2 @@ await Bun.sleep(300)

beforeAll(async () => {
// 1. Start the tunnel server
serverProc = Bun.spawn(['bun', 'run', 'server.ts'], {

@@ -74,4 +66,3 @@ cwd: import.meta.dir,

// 2. Start the demo webserver
demoProc = Bun.spawn(['bun', 'run', 'demo.ts'], {
demoProc = Bun.spawn(['bun', 'run', 'demo.tsx'], {
cwd: import.meta.dir,

@@ -83,3 +74,2 @@ env: { ...process.env, DEMO_PORT: String(DEMO_PORT) },

// Wait for both to be up
await Promise.all([

@@ -90,3 +80,2 @@ waitFor(`http://localhost:${SERVER_PORT}/?new`),

// 3. Start the tunnel client
clientProc = Bun.spawn(

@@ -112,3 +101,2 @@ [

// Wait for the tunnel to be live
await waitForTunnel()

@@ -129,3 +117,3 @@ })

const body = await res.text()
expect(body).toContain('<h1>bunl tunnel works!</h1>')
expect(body).toContain('<h1>bunl</h1>')
})

@@ -147,3 +135,2 @@

const bytes = new Uint8Array(buf)
// PNG magic bytes: 0x89 0x50 0x4E 0x47
expect(bytes[0]).toBe(0x89)

@@ -161,3 +148,2 @@ expect(bytes[1]).toBe(0x50)

const bytes = new Uint8Array(buf)
// Our fake font is 256 bytes: [0, 1, 2, ..., 255]
expect(bytes.length).toBe(256)

@@ -200,3 +186,15 @@ expect(bytes[0]).toBe(0)

expect(res.status).toBe(404)
const body = await res.text()
expect(body).toContain('Not Found')
})
test('returns landing page for root domain', async () => {
const res = await fetch(`http://localhost:${SERVER_PORT}/`, {
headers: { host: `localhost:${SERVER_PORT}` }
})
expect(res.status).toBe(200)
const body = await res.text()
expect(body).toContain('bunl')
expect(body).toContain('Expose localhost to the world')
})
})

@@ -6,3 +6,5 @@ {

"dependencies": {
"@rubriclab/config": "^0.0.24"
"@rubriclab/config": "^0.0.24",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},

@@ -12,3 +14,5 @@ "description": "Expose localhost to the world. Bun-native localtunnel.",

"@rubriclab/package": "^0.0.124",
"@types/bun": "latest"
"@types/bun": "latest",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3"
},

@@ -37,3 +41,3 @@ "homepage": "https://github.com/RubricLab/bunl#readme",

"client": "bun --watch client.ts",
"demo": "bun --watch demo.ts",
"demo": "bun --watch demo.tsx",
"dev:server": "bun --watch server.ts",

@@ -43,2 +47,3 @@ "format": "bun x biome format --write .",

"lint:fix": "bun x biome lint . --write --unsafe",
"prepare": "bun x @rubriclab/package prepare",
"server": "bun server.ts",

@@ -52,3 +57,3 @@ "test": "bun test",

"type": "module",
"version": "0.2.0"
"version": "0.2.1"
}
+15
-67
# bunl
## A Bun WebSocket re-write of LocalTunnel
Expose localhost to the world. Bun-native WebSocket tunnel.
### Usage
To try it:
```bash
bun x bunl -p 3000 -d dev.rubric.me -s my-name
bun x bunl -p 3000
```
### Development
## Options
To install dependencies:
| Flag | Short | Default | Description |
| --- | --- | --- | --- |
| `--port` | `-p` | `3000` | Local port to expose |
| `--domain` | `-d` | `bunl.sh` | Tunnel server |
| `--subdomain` | `-s` | random | Requested subdomain |
| `--open` | `-o` | `false` | Open URL in browser |
## Development
```bash
bun i
bun dev:server # tunnel server on :1234
bun demo # demo app on :3000
bun client # connect demo to server
bun test:e2e # end-to-end tests
```
To run the server:
```bash
bun dev:server
```
(Optional) to run a dummy process on localhost:3000:
```bash
bun demo
```
To run the client:
```bash
bun client -p 3000
```
With full args:
```bash
bun client --port 3000 --domain example.so --subdomain my-subdomain --open
```
Or in shortform:
```bash
bun client -p 3000 -d example.so -s my-subdomain -o
```
The options:
- `port` / `p` the localhost port to expose eg. **3000**
- `domain` / `d` the hostname of the server Bunl is running on eg. **example.so**
- `subdomain` / `s` the public URL to request eg. **my-subdomain**.example.so
- `open` / `o` to auto-open your public URL in the browser
### [WIP] Deployment
To build the client code:
```bash
bun run build
```
To deploy the server, for example on [Fly](https://fly.io):
```bash
fly launch && fly deploy
```
Making sure to set `DOMAIN` to your domain:
```bash
fly secrets set DOMAIN=example.so
```
Open to PRs!
+38
-26
import { type ServerWebSocket, serve } from 'bun'
import type { Client, TunnelInit, TunnelRequest, TunnelResponse } from './types'
import { fromBase64, toBase64, uid } from './utils'
import { fromBase64, page, toBase64, uid } from './utils'

@@ -8,10 +8,5 @@ const port = Number(Bun.env.PORT) || 1234

const domain = Bun.env.DOMAIN || `localhost:${port}`
/** The hostname portion of DOMAIN (no port) used for subdomain extraction */
const domainHost = domain.replace(/:\d+$/, '')
/** Connected tunnel clients keyed by subdomain */
const clients = new Map<string, ServerWebSocket<Client>>()
/** Pending HTTP requests waiting for a tunnel response, keyed by request ID */
const pending = new Map<

@@ -27,2 +22,24 @@ string,

const landingHtml = page(
'bunl',
`<h1>bunl</h1>
<p>Expose localhost to the world.</p>
<p><code>bun x bunl -p 3000</code></p>`
)
function notFoundHtml(subdomain: string) {
return page(
'Not Found',
`<h1>Not Found</h1>
<p>No tunnel is connected for <code>${subdomain}</code>.</p>
<p>Make sure your client is running.</p>`
)
}
const timeoutHtml = page(
'Gateway Timeout',
`<h1>Gateway Timeout</h1>
<p>The tunnel client didn't respond in time.</p>`
)
serve<Client>({

@@ -32,7 +49,5 @@ fetch: async (req, server) => {

// Client wants to register a new tunnel
if (reqUrl.searchParams.has('new')) {
const requested = reqUrl.searchParams.get('subdomain')
let id = requested || uid()
// Avoid collisions — if taken, generate a fresh one
if (clients.has(id)) id = uid()

@@ -45,13 +60,18 @@

// Public HTTP request — route to the right tunnel client.
// Use the Host header (not reqUrl.hostname) so this works behind
// reverse proxies like Fly.io where reqUrl.hostname is internal.
// Strip the DOMAIN suffix to extract the tunnel subdomain, so it
// works when the server is itself on a subdomain (e.g. bunl.rubric.sh).
const host = (req.headers.get('host') || reqUrl.hostname).replace(/:\d+$/, '')
const subdomain = host.endsWith(`.${domainHost}`) ? host.slice(0, -(domainHost.length + 1)) : ''
const client = subdomain ? clients.get(subdomain) : undefined
if (!subdomain) {
return new Response(landingHtml, {
headers: { 'content-type': 'text/html; charset=utf-8' }
})
}
const client = clients.get(subdomain)
if (!client) {
return new Response(`Tunnel "${subdomain}" not found`, { status: 404 })
return new Response(notFoundHtml(subdomain), {
headers: { 'content-type': 'text/html; charset=utf-8' },
status: 404
})
}

@@ -63,7 +83,5 @@

// Read request body as binary and encode to base64
const rawBody = await req.arrayBuffer()
const body = rawBody.byteLength > 0 ? toBase64(rawBody) : ''
// Flatten request headers
const headers: Record<string, string> = {}

@@ -83,3 +101,2 @@ req.headers.forEach((v, k) => {

// Create a promise that will be resolved when the client responds
const response = await new Promise<TunnelResponse>((resolve, reject) => {

@@ -93,7 +110,6 @@ const timer = setTimeout(() => {

client.send(JSON.stringify(message))
}).catch((err: unknown): TunnelResponse => {
const message = err instanceof Error ? err.message : String(err)
}).catch((): TunnelResponse => {
return {
body: Buffer.from(message).toString('base64'),
headers: { 'content-type': 'text/plain' },
body: Buffer.from(timeoutHtml).toString('base64'),
headers: { 'content-type': 'text/html; charset=utf-8' },
id,

@@ -106,10 +122,7 @@ status: 504,

// Decode base64 response body back to binary
const resBody = response.body ? fromBase64(response.body) : null
// Build response headers, removing problematic ones
const resHeaders = { ...response.headers }
delete resHeaders['content-encoding']
delete resHeaders['transfer-encoding']
// Fix content-length to match the actual decoded body
if (resBody) {

@@ -131,3 +144,2 @@ resHeaders['content-length'] = String(resBody.byteLength)

},
message(_ws, raw) {

@@ -134,0 +146,0 @@ const msg = JSON.parse(

{
"exclude": ["node_modules"],
"extends": "@rubriclab/config/tsconfig",
"include": ["**/*.ts"]
"include": ["**/*.ts", "**/*.tsx"]
}
export type Client = { id: string }
/** Server → Client: incoming HTTP request to proxy */
export type TunnelRequest = {

@@ -10,6 +9,5 @@ type: 'request'

headers: Record<string, string>
body: string // base64-encoded
body: string
}
/** Client → Server: proxied HTTP response */
export type TunnelResponse = {

@@ -21,6 +19,5 @@ type: 'response'

headers: Record<string, string>
body: string // base64-encoded
body: string
}
/** Server → Client: initial connection info */
export type TunnelInit = {

@@ -27,0 +24,0 @@ type: 'init'

@@ -95,3 +95,2 @@ const adjectives = [

/** Generate a human-readable unique identifier, e.g. "bold-calm-fox" */
export function uid(): string {

@@ -101,3 +100,2 @@ return `${pick(adjectives)}-${pick(adjectives)}-${pick(nouns)}`

/** Open a URL in the default browser using platform-native commands */
export function openBrowser(url: string): void {

@@ -112,3 +110,2 @@ const cmds: Record<string, string[]> = {

/** Encode an ArrayBuffer to base64 */
export function toBase64(buf: ArrayBuffer): string {

@@ -118,3 +115,2 @@ return Buffer.from(buf).toString('base64')

/** Decode a base64 string to a Uint8Array */
export function fromBase64(str: string): Uint8Array<ArrayBuffer> {

@@ -124,1 +120,35 @@ const buf = Buffer.from(str, 'base64')

}
export function page(title: string, body: string): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${title} — bunl</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #fff;
color: #000;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
main { max-width: 480px; width: 100%; }
h1 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 1rem; }
p { color: #666; line-height: 1.6; margin-bottom: 0.75rem; }
code { background: #f5f5f5; padding: 0.15em 0.4em; border-radius: 3px; font-size: 0.9em; }
a { color: #000; }
</style>
</head>
<body>
<main>
${body}
</main>
</body>
</html>`
}
/**
* Demo webserver that serves multiple content types for testing the tunnel.
* Exercises: HTML, CSS, JSON API, binary images (PNG), and binary fonts (WOFF2-like).
*/
import { serve } from 'bun'
const port = Number(Bun.env.DEMO_PORT) || 3000
// 1x1 red PNG pixel (67 bytes) — smallest valid PNG
const PNG_1PX = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
'base64'
)
// Generate a binary blob to simulate a font file
const FAKE_FONT = (() => {
const buf = Buffer.alloc(256)
for (let i = 0; i < 256; i++) buf[i] = i
return buf
})()
const CSS_CONTENT = `body { font-family: sans-serif; background: #111; color: #0f0; padding: 2rem; }
img { border: 2px solid #0f0; margin: 1rem 0; }`
const HTML = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>bunl demo</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>bunl tunnel works!</h1>
<p>This page was served through the tunnel.</p>
<img src="/image.png" alt="test pixel" width="100" height="100">
<p><a href="/api/health">JSON API</a> · <a href="/font.woff2">Font binary</a></p>
</body>
</html>`
serve({
fetch(req) {
const url = new URL(req.url)
const { pathname } = url
console.log(`${req.method} ${pathname}`)
if (pathname === '/style.css') {
return new Response(CSS_CONTENT, {
headers: { 'content-type': 'text/css; charset=utf-8' }
})
}
if (pathname === '/image.png') {
return new Response(new Uint8Array(PNG_1PX), {
headers: { 'content-type': 'image/png' }
})
}
if (pathname === '/font.woff2') {
return new Response(new Uint8Array(FAKE_FONT), {
headers: { 'content-type': 'font/woff2' }
})
}
if (pathname === '/api/health') {
return Response.json({ status: 'ok', timestamp: Date.now() })
}
if (pathname === '/echo' && req.method === 'POST') {
return new Response(req.body, {
headers: {
'content-type': req.headers.get('content-type') || 'application/octet-stream'
}
})
}
return new Response(HTML, {
headers: { 'content-type': 'text/html; charset=utf-8' }
})
},
port
})
console.log(`Demo server at http://localhost:${port}`)

Sorry, the diff of this file is not supported yet