import crypto from 'node:crypto' import fs from 'node:fs' import { stat } from 'node:fs/promises' import http from 'node:http' import path from 'node:path' import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const distDir = path.join(__dirname, 'dist') const port = Number(process.env.PORT || process.env.SERVER_PORT || 7860) const host = process.env.HOST || '0.0.0.0' const authUsername = process.env.HF_USERNAME || process.env.APP_USERNAME || '' const authPassword = process.env.HF_PASSWORD || process.env.APP_PASSWORD || '' const authEnabled = authUsername.length > 0 && authPassword.length > 0 if ((authUsername && !authPassword) || (!authUsername && authPassword)) { console.error('Set both HF_USERNAME and HF_PASSWORD to enable Basic Auth.') process.exit(1) } const mimeTypes = { '.css': 'text/css; charset=utf-8', '.gif': 'image/gif', '.html': 'text/html; charset=utf-8', '.ico': 'image/x-icon', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.mid': 'audio/midi', '.midi': 'audio/midi', '.mp3': 'audio/mpeg', '.ogg': 'audio/ogg', '.png': 'image/png', '.svg': 'image/svg+xml', '.txt': 'text/plain; charset=utf-8', '.wasm': 'application/wasm', '.wav': 'audio/wav', '.webp': 'image/webp' } function safeEqual(value, expected) { const valueBuffer = Buffer.from(value) const expectedBuffer = Buffer.from(expected) if (valueBuffer.length !== expectedBuffer.length) { return false } return crypto.timingSafeEqual(valueBuffer, expectedBuffer) } function parseBasicAuth(header) { if (!header?.startsWith('Basic ')) { return null } try { const decoded = Buffer.from(header.slice(6), 'base64').toString('utf8') const separatorIndex = decoded.indexOf(':') if (separatorIndex === -1) { return null } return { username: decoded.slice(0, separatorIndex), password: decoded.slice(separatorIndex + 1) } } catch { return null } } function isAuthorized(req) { if (!authEnabled) { return true } const credentials = parseBasicAuth(req.headers.authorization) if (!credentials) { return false } return ( safeEqual(credentials.username, authUsername) && safeEqual(credentials.password, authPassword) ) } function sendAuthChallenge(res) { res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8', 'WWW-Authenticate': 'Basic realm="Jam Track Studio", charset="UTF-8"' }) res.end('Authentication required') } function getRequestedPath(req) { const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`) let decodedPath = '/' try { decodedPath = decodeURIComponent(url.pathname) } catch { return null } const normalizedPath = path.normalize(decodedPath) const relativePath = normalizedPath === '/' || normalizedPath === '.' ? 'index.html' : normalizedPath.replace(/^[/\\]+/, '') const requestedPath = path.resolve(distDir, relativePath) const relativeToDist = path.relative(distDir, requestedPath) if (relativeToDist.startsWith('..') || path.isAbsolute(relativeToDist)) { return null } return requestedPath } async function resolveStaticPath(req) { const requestedPath = getRequestedPath(req) if (!requestedPath) { return null } try { const fileStat = await stat(requestedPath) if (fileStat.isFile()) { return requestedPath } } catch { // Fall through to SPA fallback below. } const hasFileExtension = path.extname(requestedPath).length > 0 return hasFileExtension ? null : path.join(distDir, 'index.html') } async function handleRequest(req, res) { if (!isAuthorized(req)) { sendAuthChallenge(res) return } if (req.method !== 'GET' && req.method !== 'HEAD') { res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('Method not allowed') return } const filePath = await resolveStaticPath(req) if (!filePath) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('Not found') return } const extension = path.extname(filePath).toLowerCase() const headers = { 'Content-Type': mimeTypes[extension] || 'application/octet-stream' } if (filePath.includes(`${path.sep}assets${path.sep}`)) { headers['Cache-Control'] = 'public, max-age=31536000, immutable' } res.writeHead(200, headers) if (req.method === 'HEAD') { res.end() return } fs.createReadStream(filePath).pipe(res) } const server = http.createServer((req, res) => { handleRequest(req, res).catch((error) => { console.error(error) res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('Internal server error') }) }) server.listen(port, host, () => { const authMessage = authEnabled ? 'Basic Auth enabled from HF_USERNAME/HF_PASSWORD' : 'Basic Auth disabled because HF_USERNAME/HF_PASSWORD are not both set' console.log(`Jam Track Studio serving on http://${host}:${port}`) console.log(authMessage) })