Compare commits

...

10 Commits

Author SHA1 Message Date
38d3481f8d prime time 2025-09-24 17:59:17 -07:00
5570322efa /tunnels and /tunnel/app 2025-09-24 09:54:00 -07:00
aa26b3fce5 note 2025-09-24 09:36:50 -07:00
40260ce958 update instructions 2025-09-24 09:34:03 -07:00
61d917ba7f NODE_ENV 2025-09-24 09:33:46 -07:00
0b6ff7f854 disconnect 2025-09-23 22:00:15 -07:00
a01e45b2b0 unshare 2025-09-23 22:00:10 -07:00
dff4831a81 expand "share" command 2025-09-23 21:43:52 -07:00
bdd320eab1 subdomains 2025-09-23 21:16:52 -07:00
cfa8f2795e share command 2025-09-23 21:03:10 -07:00
8 changed files with 156 additions and 41 deletions

View File

@ -9,16 +9,15 @@
## Local Dev
bun install
mkdir -p ~/nose/{bin,www}
bun dev
## Commands
Commands can return one of three types:
- string
- { error: string }
- { html: string }
- `string`
- `{ error: string }`
- `{ html: string }`
They can also `throw` to display an error.

View File

@ -1,7 +1,7 @@
import { $ } from "bun"
import { apps } from "app/src/webapp"
const devMode = process.env.BUN_HOT
const devMode = process.env.NODE_ENV !== "production"
export default async function () {
const { stdout: hostname } = await $`hostname`.quiet()

23
app/nose/bin/share.ts Normal file
View File

@ -0,0 +1,23 @@
import { apps } from "app/src/webapp"
import { connectSneaker, sneakers, sneakerUrl } from "app/src/sneaker"
export default async function (app: string, subdomain = "") {
if (!app) {
let out = `usage: share <app> [subdomain]`
const apps = sneakers()
if (apps.length) {
out += "\n\nsharing\n" + apps.map(app => {
const url = sneakerUrl(app)
return `${app}: <a href="${url}">${url}</a>`
}).join("\n")
}
return { html: out }
}
if (!apps().includes(app)) {
return { error: `${app} not found` }
}
const url = sneakerUrl(await connectSneaker(app, subdomain))
return { html: `<a href="${url}">${url}</a>` }
}

18
app/nose/bin/unshare.ts Normal file
View File

@ -0,0 +1,18 @@
import { apps } from "app/src/webapp"
import { disconnectSneaker, sneakers } from "app/src/sneaker"
export default async function (app: string) {
if (!app) {
return "usage: unshare <app>"
}
if (!apps().includes(app)) {
return { error: `${app} not found` }
}
if (!sneakers().includes(app)) {
return { error: `${app} not shared` }
}
return (await disconnectSneaker(app)) ? "unshared" : `${app} wasn't shared`
}

View File

@ -5,6 +5,7 @@
"private": true,
"scripts": {
"start": "bun src/server.tsx",
"prod": "env NODE_ENV=production bun src/server.tsx",
"dev": "env BUN_HOT=1 bun --hot src/server.tsx",
"deploy": "./scripts/deploy.sh",
"push": "./scripts/deploy.sh",

View File

@ -1,4 +1,6 @@
#!/usr/bin/env bash
# It isn't enough to modify this yet.
# You also need to manually update the nose-pluto.service file.
HOST="${HOST:-chris@nose-pluto.local}"
DEST="${DEST:-~/pluto}"

View File

@ -1,21 +1,90 @@
import app from "./server"
import nose from "./server"
const SNEAKER_URL = "localhost:3100"
const SNEAKER_URL = "nose.space"
const SNEAKER_TLS = true
// const SNEAKER_URL = "localhost:3100"
// const SNEAKER_TLS = false
const ws = new WebSocket(`ws://${SNEAKER_URL}/tunnel`)
type Connection = {
subdomain: string
ws: any
close: boolean // manual close
}
const conns: Record<string, Connection> = {}
ws.onerror = e => console.log("sneaker error", e)
export function sneakerUrl(appOrSubdomain: string): string {
let conn = conns[appOrSubdomain]
if (!conn) {
for (const appName of Object.keys(conns)) {
if (conns[appName]?.subdomain === appOrSubdomain) {
conn = conns[appName]
break
}
}
if (!conn)
return "none"
}
let url = "http" + (SNEAKER_TLS ? "s" : "") + "://"
url += conn.subdomain + "." + SNEAKER_URL
return url
}
export function sneakers(): string[] {
return Object.keys(conns)
}
export function disconnectSneaker(app: string): boolean {
if (!sneakers().includes(app) || !conns[app])
return false
conns[app].close = true
conns[app].ws.close()
return true
}
// returns the sneaker subdomain if successful
export async function connectSneaker(app: string, subdomain = ""): Promise<string> {
if (conns[app]) {
return conns[app].subdomain
}
let url = `ws${SNEAKER_TLS ? "s" : ""}://${SNEAKER_URL}/tunnel?app=${app}`
if (subdomain) url += `&subdomain=${subdomain}`
const ws = new WebSocket(url)
let resolve: (v: string) => void
let promise = new Promise<string>(res => resolve = res)
ws.onclose = e => {
if (!conns[app]?.close)
setTimeout(() => connectSneaker(app, subdomain), 1000) // simple retry
delete conns[app]
}
ws.onerror = e => console.error("sneaker error", e)
ws.onmessage = async event => {
const msg = JSON.parse(event.data.toString())
if (msg.subdomain) {
conns[app] = { subdomain: msg.subdomain, ws, close: false }
subdomain = msg.subdomain
resolve(msg.subdomain)
return
}
try {
const req = new Request("http://localhost" + msg.path, {
const req = new Request(`http://${msg.app}.localhost` + msg.path, {
method: msg.method,
headers: msg.headers,
body: msg.body || undefined,
})
const res = await app.fetch(req)
const res = await nose.fetch(req)
const body = await res.text()
const headers: Record<string, string> = {}
@ -36,3 +105,6 @@ ws.onmessage = async event => {
}))
}
}
return promise
}

View File

@ -111,7 +111,7 @@ export async function publishDNS() {
}
function publishAppDNS(app: string) {
if (process.env.BUN_HOT) return
if (process.env.NODE_ENV !== "production") return
if (!dnsEntries[app])
dnsEntries[app] = Bun.spawn(["avahi-publish", "-a", `${app}.${host}.local`, "-R", ip])