-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmiddleware.ts
More file actions
110 lines (100 loc) · 4.48 KB
/
Copy pathmiddleware.ts
File metadata and controls
110 lines (100 loc) · 4.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { defineMiddleware } from "astro:middleware";
import { tryEnv } from "./lib/runtime";
import { captureServerException } from "./lib/sentry";
import { isApexHost } from "./lib/site";
import { ADMIN_ROLES } from "./lib/oidc";
// The one host whose pages should be indexable. Every other host that serves
// this Worker (the workers.dev staging URL, www, CF preview deploys) is kept out
// of the search index via X-Robots-Tag so it never competes with the apex —
// canonical tags already point at the apex. Flip nothing here at cutover: once
// devmultigroup.com serves this Worker, the guard simply stops matching.
// Canonical-host detection is shared (isApexHost) so the noindex guard, the
// SSR Sentry capture, the client analytics gate (BaseLayout) and the server
// PostHog gate (analytics-server) all agree on one definition of "the apex".
// Cloudflare Access fronts /admin at the network edge and injects the
// authenticated user's email. This middleware surfaces that email to pages and
// acts as defence-in-depth: in production, no Access header → no admin. It also
// layers on baseline security headers, the staging-noindex guard, and a
// conservative edge cache for SSR HTML.
export const onRequest = defineMiddleware(async (context, next) => {
// /admin identity now comes from the Warden session (OIDC), not Cloudflare
// Access. Resolved lazily in the /admin branch below.
context.locals.adminEmail = null;
const dev = import.meta.env.DEV;
const { pathname, host } = context.url;
// Render the route, capturing any SSR exception to Sentry before re-throwing
// so Astro still renders its 500. Capture is fire-and-forget via waitUntil and
// skipped in dev to keep local errors out of the dashboard.
const render = async (): Promise<Response> => {
try {
return await next();
} catch (err) {
// Apex-only error capture: skip dev and any non-canonical host (staging
// workers.dev / CF previews / www) so only devmultigroup.com reports.
if (!dev && isApexHost(host)) {
const env = tryEnv(context.locals);
const wait = context.locals?.runtime?.ctx?.waitUntil?.bind(context.locals.runtime.ctx);
if (env) {
const p = captureServerException(env, err, { request: context.request });
if (wait) wait(p);
else await p;
}
}
throw err;
}
};
let response: Response;
if (pathname.startsWith("/admin")) {
// Warden OIDC session is the gate. (Cloudflare Access is retired for /admin.)
let authed: App.SessionData["auth"] | undefined;
try {
authed = await context.session?.get("auth");
} catch {
/* no session store / not signed in */
}
const role = authed?.role ?? "";
if (authed && ADMIN_ROLES.has(role)) {
context.locals.adminEmail = authed.email ?? "admin";
context.locals.adminRole = role;
response = await render();
} else if (dev) {
// Local convenience: edit content without logging in. Hit /auth/login to
// exercise the real Warden OIDC flow locally.
context.locals.adminEmail = "dev@localhost";
context.locals.adminRole = "super-admin";
response = await render();
} else {
response = context.redirect(
`/auth/login?redirect=${encodeURIComponent(pathname)}`,
302,
);
}
} else {
response = await render();
}
const h = response.headers;
// Baseline security headers (trust signals; zero functional risk).
h.set("X-Content-Type-Options", "nosniff");
h.set("Referrer-Policy", "strict-origin-when-cross-origin");
h.set("X-Frame-Options", "SAMEORIGIN");
h.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
if (!dev) h.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
// Keep non-canonical hosts (staging/preview/www) out of the index.
if (!dev && !isApexHost(host)) {
h.set("X-Robots-Tag", "noindex, nofollow");
}
// Short edge cache for SSR HTML — content is KV-cached (≤600s TTL) behind this,
// so a 60s shared cache + SWR is safe and offloads the Worker for crawlers.
// Skips admin/api and never overrides a route that set its own cache-control.
if (
context.request.method === "GET" &&
response.status === 200 &&
!pathname.startsWith("/admin") &&
!pathname.startsWith("/api") &&
!h.has("cache-control") &&
(h.get("content-type") || "").includes("text/html")
) {
h.set("Cache-Control", "public, max-age=0, s-maxage=60, stale-while-revalidate=300");
}
return response;
});