// Static asset serving for favela's browser-side JS (the sign-in client + vendored crypto).
// Serves files from `web/static/` over `GET /static/<path>`. Bare-native (bare-fs/bare-path/bare-url).
//
// Adapted from pear-git-v3's lib/web/static.js: extension allowlist (not a per-file whitelist) +
// path-traversal guard, so a client can never read arbitrary files. Content-Length is intentionally
// NOT set — res.end(body) derives it; setting it explicitly makes bare-http1 emit a duplicate header
// its (stricter-than-node) parser rejects.
import { readFile } from 'bare-fs/promises'
import { fileURLToPath } from 'bare-url'
import { dirname, join, normalize } from 'bare-path'
import { STATIC_ASSETS } from '../web/static-assets.js'
const __dirname = dirname(fileURLToPath(import.meta.url))
const STATIC_DIR = join(__dirname, '..', 'web', 'static')
const ALLOWED_EXTENSIONS = new Set(['.js', '.css', '.json', '.map'])
const CONTENT_TYPES = {
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json'
}
function contentType (path) {
const dot = path.lastIndexOf('.')
if (dot < 0) return 'application/octet-stream'
return CONTENT_TYPES[path.slice(dot)] || 'application/octet-stream'
}
// Serve a static file from web/static/. Returns true if handled, false if unknown/blocked (caller 404s).
// relPath is the path AFTER /static/, e.g. "vendor/noble-secp256k1.js".
export async function serveStatic (relPath, res) {
if (!relPath || typeof relPath !== 'string') return false
const clean = relPath.replace(/^\/+/, '')
const normalized = normalize(clean)
if (normalized.startsWith('..') || normalized.includes('/..')) return false
if (normalized !== clean) return false
const dot = normalized.lastIndexOf('.')
const ext = dot >= 0 ? normalized.slice(dot) : ''
if (!ALLOWED_EXTENSIONS.has(ext)) return false
// ⛔ FILESYSTEM FIRST, BUNDLE SECOND. On a server web/static/ is a real directory and this read is the
// whole story. Inside a bundle it is not: bare-pack packs the module GRAPH, these files are imported by
// nothing, and readFile throws — which is exactly what favela-android has been doing since the WebView
// pivot. Measured on the handset 2026-08-18: /static/compose.js and /static/actions.js both 404, so
// image attach, like, repost, follow, drafts and reporting had NEVER worked there. The markup arrived
// for free and the behaviour silently did not.
//
// The generated module is the copy that survives packing. Filesystem stays first so a server picks up
// an edited file without regenerating, and test/static-assets.test.mjs fails if the two ever disagree.
let body
try { body = await readFile(join(STATIC_DIR, normalized)) } catch { body = null }
if (body === null) {
const src = STATIC_ASSETS[normalized]
if (src === undefined) return false
body = Buffer.from(src, 'utf8')
}
res.writeHead(200, { 'Content-Type': contentType(normalized), 'Cache-Control': 'no-cache, must-revalidate' })
res.end(body)
return true
}