Skip to content
File

Blob: src/worker/router.ts

typescript96 lines
1import { Hono } from "hono";
2 
3import { privateRoutes } from "@/worker/api/private/router";
4import { publicRoutes } from "@/worker/api/public/router";
5import { D1_BOOKMARK_HEADER, openSession } from "@/worker/db/d1";
6import type { AppEnv } from "@/worker/hono";
7import { HttpError, toErrorResponse } from "@/worker/http";
8import { createLogger } from "@/worker/services";
9 
10const logger = createLogger("worker.router");
11 
12export const app = new Hono<AppEnv>();
13 
14const buildContentSecurityPolicy = (requestUrl: string): string => {
15 const { host } = new URL(requestUrl);
16 
17 return [
18 "default-src 'self'",
19 "base-uri 'self'",
20 "form-action 'self'",
21 "frame-ancestors 'none'",
22 "object-src 'none'",
23 "script-src 'self'",
24 "style-src 'self' https://fonts.googleapis.com",
25 "font-src 'self' https://fonts.gstatic.com",
26 "img-src 'self'",
27 `connect-src 'self' ws://${host} wss://${host}`,
28 "frame-src 'none'",
29 "manifest-src 'self'",
30 "worker-src 'self'",
31 ].join("; ");
32};
33 
34const REPLICA_FRIENDLY_ROUTES = [
35 /^\/api\/private\/me$/,
36 /^\/api\/private\/projects$/,
37 /^\/api\/private\/projects\/[^/]+$/,
38 /^\/api\/private\/projects\/[^/]+\/webhooks$/,
39 /^\/api\/private\/projects\/[^/]+\/runs$/,
40 /^\/api\/private\/runs\/[^/]+$/,
41];
42const REPLICA_FRIENDLY_POST_ROUTES = [/^\/api\/private\/runs\/[^/]+\/log-ticket$/];
43 
44function selectSessionConstraint(method: string, path: string): D1SessionConstraint {
45 if (
46 (method === "GET" && REPLICA_FRIENDLY_ROUTES.some((route) => route.test(path))) ||
47 (method === "POST" && REPLICA_FRIENDLY_POST_ROUTES.some((route) => route.test(path)))
48 ) {
49 return "first-unconstrained";
50 }
51 
52 return "first-primary";
53}
54 
55app.use("*", async (c, next) => {
56 await next();
57 c.header("X-Content-Type-Options", "nosniff");
58 c.header("X-Frame-Options", "DENY");
59 c.header("Strict-Transport-Security", "max-age=63072000; includeSubDomains");
60 c.header("X-Permitted-Cross-Domain-Policies", "none");
61 c.header("Referrer-Policy", "strict-origin-when-cross-origin");
62 c.header("Content-Security-Policy", buildContentSecurityPolicy(c.req.url));
63});
64 
65app.use("*", async (c, next) => {
66 const bookmark = c.req.header(D1_BOOKMARK_HEADER)?.trim();
67 const d1 = openSession(c.env, bookmark || selectSessionConstraint(c.req.method, c.req.path));
68 
69 c.set("db", d1.db);
70 
71 try {
72 await next();
73 } finally {
74 const nextBookmark = d1.getBookmark();
75 if (nextBookmark) {
76 c.header(D1_BOOKMARK_HEADER, nextBookmark);
77 }
78 }
79});
80 
81app.route("/api/public", publicRoutes);
82app.route("/api/private", privateRoutes);
83 
84app.notFound((c) => toErrorResponse(c, new HttpError(404, "not_found", "Not found.")));
85 
86app.onError((error, c) => {
87 logger.error("request_failed", {
88 method: c.req.method,
89 path: c.req.path,
90 error: error instanceof Error ? error.message : String(error),
91 stack: error instanceof Error ? (error.stack ?? null) : null,
92 });
93 
94 return toErrorResponse(c, error);
95});