benjivers/notes Public Public: anyone can read this; nobody can change it.
Files 7260e8fc5b5c3ca9332ff3506ef97626f456e658
| 1 | /** |
| 2 | * The process the forge runs. `serve` opens the database and listens: |
| 3 | * every request gets the floor (`http.ts`), an identity (`who.ts`), the |
| 4 | * door (`security.ts`) and one route (`routes.ts`) — so a request from |
| 5 | * another site, from someone going too fast, or from someone the contract |
| 6 | * turns away is refused before a byte of it is read. Run directly, it |
| 7 | * serves what the forge said; a test calls `serve` with its own. |
| 8 | */ |
| 9 | |
| 10 | import { openDb } from "./db.ts"; |
| 11 | import { dataDir, identityKey, title, where } from "./env.ts"; |
| 12 | import { html, text } from "./http.ts"; |
| 13 | import { match } from "./routes.ts"; |
| 14 | import { door } from "./security.ts"; |
| 15 | import { who } from "./who.ts"; |
| 16 | |
| 17 | export interface ServeOptions { |
| 18 | readonly port: number; |
| 19 | readonly hostname?: string | undefined; |
| 20 | readonly dataDir: string; |
| 21 | readonly identityKey: string; |
| 22 | } |
| 23 | |
| 24 | export function serve(options: ServeOptions) { |
| 25 | const db = openDb(options.dataDir); |
| 26 | return Bun.serve({ |
| 27 | port: options.port, |
| 28 | hostname: options.hostname ?? "127.0.0.1", |
| 29 | // A connection that says nothing for a minute is dropped. The event |
| 30 | // stream heartbeats every 25 seconds to stay well inside that. |
| 31 | idleTimeout: 60, |
| 32 | async fetch(request, server) { |
| 33 | const url = new URL(request.url); |
| 34 | const me = who(request, options.identityKey); |
| 35 | const refused = door( |
| 36 | db, |
| 37 | request, |
| 38 | url, |
| 39 | me, |
| 40 | server.requestIP(request)?.address ?? "?", |
| 41 | ); |
| 42 | if (refused !== undefined) return head(request, refused); |
| 43 | const found = match( |
| 44 | request.method === "HEAD" ? "GET" : request.method, |
| 45 | url.pathname, |
| 46 | ); |
| 47 | if (found === undefined) return text("not found", 404); |
| 48 | const response = await found.route.handle({ |
| 49 | request, |
| 50 | url, |
| 51 | db, |
| 52 | who: me, |
| 53 | params: found.params, |
| 54 | }); |
| 55 | return head(request, response); |
| 56 | }, |
| 57 | // A bug answers with the floor and nothing else: never the runtime's |
| 58 | // own error page, which carries source and paths, and never without |
| 59 | // the headers. The detail goes to the log, where it belongs. |
| 60 | error(error) { |
| 61 | console.error(error); |
| 62 | return html("<h1>Something went wrong</h1>", 500); |
| 63 | }, |
| 64 | }); |
| 65 | } |
| 66 | |
| 67 | // HEAD is GET without the body: a health check sees the app up. |
| 68 | const head = (request: Request, response: Response): Response => |
| 69 | request.method === "HEAD" |
| 70 | ? new Response(null, { status: response.status, headers: response.headers }) |
| 71 | : response; |
| 72 | |
| 73 | if (import.meta.main) { |
| 74 | const server = serve({ |
| 75 | port: Number(process.env.PORT ?? 8080), |
| 76 | hostname: process.env.HOST, |
| 77 | dataDir, |
| 78 | identityKey, |
| 79 | }); |
| 80 | console.log( |
| 81 | `${title} listening on http://${server.hostname}:${server.port} (${where})`, |
| 82 | ); |
| 83 | } |