benjivers/notes Public Public: anyone can read this; nobody can change it.

Files main
101 lines · 3.5 KB · ~1.0k tok Raw History
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, forgeApi, identityKey, repoToken, title, where } from "./env.ts";
12
import { sessionWho } from "./accounts.ts";
13
import { html, readCookie, text } from "./http.ts";
14
import { SESSION_COOKIE } from "./routes.ts";
15
import { match } from "./routes.ts";
16
import { door } from "./security.ts";
17
import { httpForge, startSync, type Forge } from "./sync.ts";
18
import { who } from "./who.ts";
19
import { traceServe } from "./otel.ts";
20
21
export interface ServeOptions {
22
  readonly port: number;
23
  readonly hostname?: string | undefined;
24
  readonly dataDir: string;
25
  readonly identityKey: string;
26
  /** Where shared folders commit; without one there are none. */
27
  readonly forge?: Forge | undefined;
28
}
29
30
export function serve(options: ServeOptions) {
31
  const db = openDb(options.dataDir);
32
  const stopSync = startSync(db, options.forge);
33
  const server = Bun.serve(traceServe({
34
    port: options.port,
35
    hostname: options.hostname ?? "127.0.0.1",
36
    // A connection that says nothing for a minute is dropped. The event
37
    // stream heartbeats every 25 seconds to stay well inside that.
38
    idleTimeout: 60,
39
    async fetch(request, server) {
40
      const url = new URL(request.url);
41
      // The forge's signed word first; else the app's own session, if any.
42
      const signed = who(request, options.identityKey);
43
      const me =
44
        signed.role === "anon" ? sessionWho(db, readCookie(request, SESSION_COOKIE)) : signed;
45
      const refused = door(
46
        db,
47
        request,
48
        url,
49
        me,
50
        server.requestIP(request)?.address ?? "?",
51
      );
52
      if (refused !== undefined) return head(request, refused);
53
      const found = match(
54
        request.method === "HEAD" ? "GET" : request.method,
55
        url.pathname,
56
      );
57
      if (found === undefined) return text("not found", 404);
58
      const response = await found.route.handle({
59
        request,
60
        url,
61
        db,
62
        who: me,
63
        params: found.params,
64
        forge: options.forge,
65
      });
66
      return head(request, response);
67
    },
68
    // A bug answers with the floor and nothing else: never the runtime's
69
    // own error page, which carries source and paths, and never without
70
    // the headers. The detail goes to the log, where it belongs.
71
    error(error) {
72
      console.error(error);
73
      return html("<h1>Something went wrong</h1>", 500);
74
    },
75
  }));
76
  const stop = server.stop.bind(server);
77
  server.stop = (force?: boolean) => {
78
    stopSync();
79
    return stop(force);
80
  };
81
  return server;
82
}
83
84
// HEAD is GET without the body: a health check sees the app up.
85
const head = (request: Request, response: Response): Response =>
86
  request.method === "HEAD"
87
    ? new Response(null, { status: response.status, headers: response.headers })
88
    : response;
89
90
if (import.meta.main) {
91
  const server = serve({
92
    port: Number(process.env.PORT ?? 8080),
93
    hostname: process.env.HOST,
94
    dataDir,
95
    identityKey,
96
    forge: repoToken === undefined || forgeApi === undefined ? undefined : httpForge(forgeApi, repoToken),
97
  });
98
  console.log(
99
    `${title} listening on http://${server.hostname}:${server.port} (${where})`,
100
  );
101
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts