// HTTP surface: the page, and a read-only JSON API.
//
// GET /api/host this host's services (cached ~15 s)
// GET /api/lan the last LAN scan + whether one is running
// POST /api/lan/scan start a LAN scan now
//
// ⛔ No login: LAN-only, like the services it lists. The one POST must carry
// `x-host-services: 1` -- a cross-site page cannot add a custom header without
// a CORS preflight, and this server never answers one.
import http from 'bare-http1'
import fs from 'bare-fs'
import path from 'bare-path'
const STATIC_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'static')
const ASSETS = {
'/': ['index.html', 'text/html; charset=utf-8'],
'/index.html': ['index.html', 'text/html; charset=utf-8'],
'/app.js': ['app.js', 'text/javascript; charset=utf-8'],
'/app.css': ['app.css', 'text/css; charset=utf-8']
}
function sendJSON (res, status, obj) {
const body = JSON.stringify(obj)
res.writeHead(status, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'cache-control': 'no-store' })
res.end(body)
}
export function createServer ({ host, lan }) {
return http.createServer((req, res) => {
const url = req.url.split('?')[0]
if (url === '/api/host' && req.method === 'GET') {
host.snapshot().then((s) => sendJSON(res, 200, s), (err) => {
console.error('host snapshot failed: ' + (err.stack || err))
sendJSON(res, 500, { error: err.message })
})
return
}
if (url === '/api/lan' && req.method === 'GET') return sendJSON(res, 200, lan.state())
if (url === '/api/lan/scan' && req.method === 'POST') {
if (req.headers['x-host-services'] !== '1') return sendJSON(res, 403, { error: 'missing x-host-services header' })
lan.scan()
return sendJSON(res, 202, { ok: true, scanning: true })
}
if (url.startsWith('/api/')) return sendJSON(res, 404, { error: 'not found' })
const asset = ASSETS[url]
if (!asset || req.method !== 'GET') {
res.writeHead(404, { 'content-type': 'text/plain' })
return res.end('not found')
}
let body
try {
body = fs.readFileSync(path.join(STATIC_DIR, asset[0]))
} catch {
res.writeHead(500, { 'content-type': 'text/plain' })
return res.end('asset missing: ' + asset[0])
}
res.writeHead(200, { 'content-type': asset[1], 'content-length': body.length, 'cache-control': 'no-cache' })
res.end(body)
})
}