// server.js — the bare-http1 adapter. Deliberately thin: it reads the request, calls the router,
// writes the response. All decisions live in router.js, which is testable without a socket.
import http from 'bare-http1'
import { handleRequest } from './router.js'
const MAX_BODY = 1024 * 1024 // an inbox POST is a small JSON document; anything larger is abuse
export function createServer (ctx) {
return http.createServer(async (req, res) => {
let out
try {
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`)
const body = await readBody(req)
if (body === TOO_LARGE) {
res.writeHead(413, { 'content-type': 'text/plain' })
return res.end('payload too large')
}
out = await handleRequest({
method: req.method,
path: url.pathname,
query: Object.fromEntries(url.searchParams),
headers: req.headers,
body
}, ctx)
} catch (err) {
// A crash here would take the daemon down on a malformed request from anyone on the
// internet. Log it and return 500 — never let an inbound request kill the process.
ctx.log?.({ msg: 'unhandled error', error: err.message, stack: err.stack })
out = { status: 500, headers: { 'content-type': 'text/plain' }, body: 'internal error' }
}
// ⛔ LOG WHAT THE FEDIVERSE ASKS US FOR. There was no request log at all, so "prsm.space accepted
// our activity and shows nothing" had two indistinguishable explanations — it never dereferenced
// our actor or objects, or it did and got something wrong — and no way to tell them apart from
// this side. Method, path, status and user-agent, one line; the same reasoning as the mirror's
// trigger counters and the drain's empty-pass line.
//
// ⚠ Deliberately NOT the body, and not headers: an inbox POST carries a signature and a private
// message could carry content. What is logged is what a normal access log holds.
try {
const ua = String(req.headers['user-agent'] || '')
ctx.log?.({ msg: 'req', method: req.method, path: new URL(req.url, 'http://x').pathname, status: out.status, ua: ua.slice(0, 80) })
} catch {}
res.writeHead(out.status, out.headers || {})
res.end(out.body || '')
})
}
const TOO_LARGE = Symbol('too-large')
function readBody (req) {
return new Promise((resolve, reject) => {
if (req.method === 'GET' || req.method === 'HEAD') return resolve(null)
const chunks = []
let size = 0
req.on('data', (c) => {
size += c.length
if (size > MAX_BODY) { resolve(TOO_LARGE); req.destroy(); return }
chunks.push(c)
})
req.on('end', () => resolve(chunks.length ? Buffer.concat(chunks).toString('utf8') : null))
req.on('error', reject)
})
}