Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137import https from 'node:https'import http from 'node:http'
const ROUTE = '/api/feedback/board'const DEFAULT_ORIGIN = 'https://userinput.app'const MAX_RESPONSE_BYTES = 2_000_000const TIMEOUT_MS = 10_000const DID_PATTERN = /^did:[a-z]+:[a-zA-Z0-9._:%-]{1,300}$/const RKEY_PATTERN = /^(?!\.{1,2}$)[A-Za-z0-9._~:-]{1,512}$/
/** * Create an attachable Node HTTP handler for one configured, public userinput board. * The caller never controls the upstream host or path identifiers. */export function createUserinputBoardProxyHandler({ spaceDid, spaceRkey, apiBaseUrl = DEFAULT_ORIGIN, allowLoopbackTest = false, timeoutMs = TIMEOUT_MS, maxResponseBytes = MAX_RESPONSE_BYTES,} = {}) { const upstream = resolveUpstream(apiBaseUrl, spaceDid, spaceRkey, allowLoopbackTest)
return async function userinputBoardProxy(req, res) { const pathname = new URL(req.url ?? '/', 'http://reams.invalid').pathname if (pathname !== ROUTE) return false if (req.method !== 'GET') { sendJson(res, 405, { error: 'method_not_allowed', message: 'Use GET to read the public feedback board.' }, { allow: 'GET' }) return true }
try { const result = await requestBoard(upstream, { timeoutMs, maxResponseBytes }) sendJson(res, result.status, result.status === 200 && result.body && typeof result.body === 'object' && !Array.isArray(result.body) ? { ...result.body, spaceUrl: `https://userinput.app/s/${spaceDid}/${spaceRkey}` } : result.body) } catch (error) { const status = error.code === 'UPSTREAM_TOO_LARGE' ? 502 : error.code === 'UPSTREAM_TIMEOUT' ? 504 : 502 sendJson(res, status, { error: status === 504 ? 'feedback_upstream_timeout' : 'feedback_upstream_unavailable', message: status === 504 ? 'The feedback board took too long to respond. Try again shortly.' : 'The feedback board is temporarily unavailable. Check the server’s userinput board configuration and try again.', }) } return true }}
function resolveUpstream(apiBaseUrl, spaceDid, spaceRkey, allowLoopbackTest) { if (typeof spaceDid !== 'string' || !DID_PATTERN.test(spaceDid) || typeof spaceRkey !== 'string' || !RKEY_PATTERN.test(spaceRkey)) { throw new TypeError('Configure a valid userinput board DID and record key.') } let base try { base = new URL(apiBaseUrl) } catch { throw new TypeError('Configure a valid userinput API origin.') } const loopback = base.protocol === 'http:' && allowLoopbackTest === true && ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname) if (base.username || base.password || base.search || base.hash || base.pathname !== '/' || (base.protocol !== 'https:' && !loopback)) { throw new TypeError('Userinput API origin must be HTTPS (loopback HTTP is test-only).') } // A DID's colons are part of the public route syntax (URL.pathname setter // would percent-encode them); values are still validated before assembly. return new URL(`${base.origin}/api/board/${spaceDid}/${encodeURIComponent(spaceRkey)}`)}
function requestBoard(url, { timeoutMs, maxResponseBytes }) { return new Promise((resolve, reject) => { let settled = false const finish = (error, value) => { if (settled) return settled = true error ? reject(error) : resolve(value) } const transport = url.protocol === 'https:' ? https : http const request = transport.request(url, { method: 'GET', headers: { accept: 'application/json' }, timeout: timeoutMs, }, response => { const chunks = [] let size = 0 response.on('data', chunk => { size += chunk.length if (size > maxResponseBytes) { const error = Object.assign(new Error('Upstream response exceeds size limit.'), { code: 'UPSTREAM_TOO_LARGE' }) response.destroy(error) finish(error) return } chunks.push(chunk) }) response.on('error', error => finish(error)) response.on('end', () => { if (response.statusCode === 503) { finish(null, { status: 503, body: { error: 'feedback_upstream_unavailable', message: 'The feedback board service is temporarily unavailable. Try again shortly.' } }) return } if (response.statusCode < 200 || response.statusCode >= 300) { finish(null, { status: 502, body: { error: 'feedback_upstream_unavailable', message: 'The feedback board service returned an unexpected response. Try again shortly.' } }) return } const contentType = String(response.headers['content-type'] ?? '').split(';', 1)[0].trim().toLowerCase() if (contentType !== 'application/json' && !contentType.endsWith('+json')) { finish(null, { status: 502, body: { error: 'feedback_upstream_invalid_response', message: 'The feedback board service returned an invalid response. Try again shortly.' } }) return } try { finish(null, { status: 200, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) }) } catch { finish(null, { status: 502, body: { error: 'feedback_upstream_invalid_response', message: 'The feedback board service returned invalid JSON. Try again shortly.' } }) } }) }) request.on('timeout', () => { const error = Object.assign(new Error('Upstream request timed out.'), { code: 'UPSTREAM_TIMEOUT' }) request.destroy(error) finish(error) }) request.on('error', error => finish(error)) request.end() })}
function sendJson(res, status, payload, extraHeaders = {}) { const body = JSON.stringify(payload) res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', ...extraHeaders, }) res.end(body)}