OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface / index.js

78 lines · javascript ref:
// favela-surface — the renderer. `bare index.js`.
//
// It holds a socket to favela-peer and nothing else: no storage, no keys, no swarm. Every page is a
// question asked of the daemon; every body is HTML that favela-markdown produced.
//
// ⛔ Binds 127.0.0.1 by default. This is your own daemon's data with NO authentication in front of
// it — exposing it must be a decision, never a default.

import http from 'bare-http1'
import os from '#node:os'
import process from '#node:process'
import { join } from '#node:path'
import { Client } from './lib/client.js'
import { Sessions } from './lib/session.js'
import { route } from './lib/router.js'
import * as pages from './lib/pages.js'

const storage = process.env.FAVELA_STORAGE || join(os.homedir(), '.favela')
const sock = process.env.FAVELA_SOCK || join(storage, 'favela.sock')
// ⛔ 4042, not 3400. The ActivityPub bridge's AP_PORT default is 3400 and it is LIVE on stortron
// behind ap.7rnx.net, so the two collided on every host running both — measured, not assumed:
// `ss -tln` showed 3400 already bound on tabertron AND stortron. 404x is the favela HTTP family
// (hypernote 4040, favela-web 4041), and 4042 was free on both.
//
// ⚠ The bridge is named in prose, not by its package name, because test/boundary.test.js greps
// this file's TEXT for projection dependencies and a mention would trip it. That guard is meant to
// be blunt; the comment moves, not the guard.
const port = Number(process.env.FAVELA_SURFACE_PORT || 4042)
const host = process.env.FAVELA_SURFACE_HOST || '127.0.0.1'

// ⛔ The DEFAULT context, not the only one. `route` takes its daemon client and its session store
// as an argument, because on a handset there is no second process to open a socket to.
//
// favela-peer's `lib/rpc.js` exports `dispatch(node, cmd, a)` alongside `createServer(node, sock)`:
// `dispatch` is the interface and the socket is one ADAPTER over it, correct on a desktop where the
// surface and the daemon really are two processes. In a bare-kit worklet they are one, so the
// adapter has nothing left to adapt — and a pathname unix socket could not be created there anyway,
// because Android SELinux refuses an unprivileged app a `sock_file`.
//
// A context is anything with `call(cmd, args) -> Promise`. This file never learns which it got.
//
// ⚠ favela-surface must NOT depend on favela-peer — the boundary test forbids reaching for storage
// or a swarm, and importing the daemon would drag both in. So the in-process client is built by
// whoever holds both halves (the worklet), never here.
const client = new Client(sock)
const sessions = new Sessions()
const DEFAULT_CTX = { client, sessions }

// Browser assets, read once at boot — they never change while the process runs, and a per-request
// read would be a filesystem call on every page.
//

const server = http.createServer((req, res) => {
  route(req, res, DEFAULT_CTX).catch((err) => {
    // ⚠ The reason, on stderr. A 502 page that says nothing turns any error in `route` into a
    // uniform blank wall — a test then reports "expected 200, got 502" and the cause is invisible.
    console.error('route failed: ' + (err && err.stack ? err.stack : err))
    // The daemon being down is the common failure and it must SAY so — a blank page would look
    // like an empty timeline, which is a different fact entirely.
    res.writeHead(502, { 'content-type': 'text/html; charset=utf-8' })
    res.end(pages.error(err.message))
  })
})

server.listen(port, host, () => {
  console.log(JSON.stringify({ msg: 'favela-surface up', url: 'http://' + host + ':' + port, sock }))
})

// ⛔ Exported so a test can drive REAL routes and then close the listener. Every authentication gap
// this surface has had was a daemon method with no route, and a unit test on a page function
// structurally cannot see one — it never asks what the server does with a request.
//
// ⚠ Importing THIS file starts the server, which is what the launcher wants and what an embedder
// does not. Anyone wanting the routes without a listener imports `lib/router.js` — that split
// exists because importing the entrypoint to reach `route` also bound its port, and on a machine
// already running favela-surface it simply threw EADDRINUSE.
export { server, route, DEFAULT_CTX }