Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

68 linesCodeBlameRaw
1import { RouterContextProvider, createRequestHandler } from "react-router";
2
3import { authorize, isSameOrigin, readSettings } from "../app/lib/access";
4import { denied, secure } from "../app/lib/guard";
5import { staffContext, zoneContext } from "../app/lib/staff";
6import { readZone } from "../app/lib/time";
7
8const requestHandler = createRequestHandler(() => import("virtual:react-router/server-build"), import.meta.env.MODE);
9
10/** Files the build emits for the pages; still behind the same check. */
11const ASSET = /^\/(?:assets\/[\w.-]+|favicon\.svg)$/;
12
13/**
14 * Every request, assets included, passes the same gate before anything
15 * is served:
16 *
17 * 1. sudo is configured, or nothing is served at all;
18 * 2. Cloudflare Access's token verifies (signature, audience, issuer, time);
19 * 3. its email is on the staff list;
20 * 4. a change is a POST from sudo's own pages.
21 */
22async function handle(request: Request, env: Env): Promise<Response> {
23 const settings = readSettings(env);
24 if (!settings) {
25 return denied(
26 403,
27 "sudo is not configured",
28 "ACCESS_TEAM_DOMAIN, ACCESS_AUD and STAFF_EMAILS must all be set before sudo will answer. See apps/sudo/README.md.",
29 );
30 }
31
32 const auth = await authorize(request, settings);
33 if (!auth.ok) {
34 console.warn(JSON.stringify({ event: "sudo.denied", reason: auth.reason, email: auth.email ?? null, path: new URL(request.url).pathname }));
35 return auth.reason === "not staff"
36 ? denied(403, "Not staff", `${auth.email} is signed in, but is not on sudo's staff list.`)
37 : denied(403, "Not allowed", "sudo is for g1t staff, signed in through Cloudflare Access.");
38 }
39
40 const { method } = request;
41 if (method !== "GET" && method !== "HEAD" && method !== "POST") {
42 return denied(405, "Method not allowed", "sudo takes GET and POST only.");
43 }
44 if (method === "POST" && !isSameOrigin(request)) {
45 console.warn(JSON.stringify({ event: "sudo.cross_site", email: auth.email, origin: request.headers.get("origin") }));
46 return denied(403, "Refused", "Changes are only accepted from sudo's own pages.");
47 }
48
49 const { pathname } = new URL(request.url);
50 if (method !== "POST" && ASSET.test(pathname)) {
51 return env.ASSETS.fetch(request);
52 }
53
54 if (method === "POST") {
55 console.log(JSON.stringify({ event: "sudo.change", email: auth.email, path: pathname }));
56 }
57 const context = new RouterContextProvider();
58 context.set(staffContext, { email: auth.email });
59 // Times in the staff member's zone: their choice (/timezone), else where Cloudflare places them.
60 context.set(zoneContext, readZone(request.headers.get("cookie"), (request as { cf?: { timezone?: unknown } }).cf?.timezone));
61 return requestHandler(request, context);
62}
63
64export default {
65 async fetch(request, env) {
66 return secure(await handle(request, env));
67 },
68} satisfies ExportedHandler<Env>;