Skip to content
File

Blob: src/worker/app.ts

typescript119 lines
1import { Hono } from "hono";
2import { requestId } from "hono/request-id";
3import { reactRenderer } from "@hono/react-renderer";
4import { describeRoute, resolver } from "hono-openapi";
5import YAML from "yaml";
6 
7import { handleSubjectDavRequest } from "@/worker/dav/subject";
8import { HealthResponseSchema } from "@/worker/openapi/schemas";
9import { buildOpenApiDocument } from "@/worker/openapi/document";
10import { requestLogging } from "@/worker/middleware/request-logging";
11import { createApiRoutes } from "@/worker/routes/api";
12import { classifyHost } from "@/worker/routes/host";
13import type { AppEnv } from "@/worker/types";
14 
15export function createApp(): Hono<AppEnv> {
16 const app = new Hono<AppEnv>();
17 
18 app.use("*", reactRenderer());
19 
20 app.use(
21 "*",
22 requestId({
23 headerName: "",
24 generator: (c) => c.req.raw.headers.get("cf-ray") ?? crypto.randomUUID(),
25 }),
26 );
27 
28 app.use("*", async (c, next) => {
29 c.set("hostInfo", classifyHost(c.req.raw, c.env));
30 await next();
31 });
32 
33 app.use("*", requestLogging());
34 
35 app.get(
36 "/healthz",
37 describeRoute({
38 hide: true,
39 responses: {
40 200: {
41 description: "Control-plane health status",
42 content: {
43 "application/json": {
44 schema: resolver(HealthResponseSchema),
45 },
46 },
47 },
48 },
49 }),
50 (c) => {
51 if (c.get("hostInfo").kind !== "control") return c.notFound();
52 return c.json({ ok: true, service: "dab" } as const);
53 },
54 );
55 
56 const api = createApiRoutes();
57 app.route("/api/v1", api);
58 
59 if (import.meta.env.DEV) {
60 app.post("/__dab_test/fixtures/:fixture", async (c) => {
61 const { handleTestFixtureRequest } = await import("@/worker/routes/test-fixtures");
62 return await handleTestFixtureRequest(c);
63 });
64 }
65 
66 app.get(
67 "/api/v1/openapi.json",
68 describeRoute({
69 summary: "Get OpenAPI JSON document",
70 tags: ["openapi"],
71 responses: {
72 200: {
73 description: "OpenAPI JSON document",
74 },
75 },
76 }),
77 async (c) => {
78 if (c.get("hostInfo").kind !== "control") return c.notFound();
79 return c.json(await buildOpenApiDocument(app, c));
80 },
81 );
82 
83 app.get(
84 "/api/v1/openapi.yaml",
85 describeRoute({
86 summary: "Get OpenAPI YAML document",
87 tags: ["openapi"],
88 responses: {
89 200: {
90 description: "OpenAPI YAML document",
91 },
92 },
93 }),
94 async (c) => {
95 if (c.get("hostInfo").kind !== "control") return c.notFound();
96 const document = await buildOpenApiDocument(app, c);
97 return c.text(YAML.stringify(document, { aliasDuplicateObjects: false }), 200, {
98 "Content-Type": "text/yaml; charset=utf-8",
99 });
100 },
101 );
102 
103 app.all("*", async (c) => {
104 const hostInfo = c.get("hostInfo");
105 if (hostInfo.kind === "subject") return await handleSubjectDavRequest(c);
106 // On the control plane only browser-navigation methods reach the SPA.
107 // DAV verbs (PROPFIND, REPORT, PROPPATCH, LOCK, ...) on control-plane
108 // non-API paths are misrouted clients — fall through to 404 so they fail
109 // closed instead of receiving the SPA shell.
110 const method = c.req.method;
111 if (hostInfo.kind === "control" && (method === "GET" || method === "HEAD" || method === "OPTIONS")) {
112 return await c.env.ASSETS.fetch(c.req.raw);
113 }
114 return c.notFound();
115 });
116 
117 return app;
118}