History

Bypass Start for marketing requests (#1637)

Rhys Sullivan committed 2 months ago

Commit ffb28cc1 parent 77f7ac35 files · +97 −52Browse files

Unified · Split

5 files
  1. apps/cloud/src/edge/index.ts +4 −5
  2. apps/cloud/src/edge/marketing.test.ts +52 −1
  3. apps/cloud/src/edge/marketing.ts +22 −34
  4. apps/cloud/src/server.ts +9 −0
  5. apps/cloud/src/start.ts +10 −12
apps/cloud/src/edge/index.ts+4−5
@@ -1,11 +1,10 @@
11 // ---------------------------------------------------------------------------
2−// Edge concerns — the analytics/marketing/docs request middlewares that run at
3−// the worker edge BEFORE the app's own mcp + api dispatch. None of these touch
4−// the Effect app layer; they proxy or tunnel to external services (the
5−// marketing worker, Sentry, PostHog, Mintlify docs).
2+// Edge concerns — request middleware that runs before the app's own mcp + api
3+// dispatch. These proxy or tunnel to external services without touching the
4+// Effect app layer. Marketing is dispatched even earlier, from server.ts, so a
5+// public page never loads the TanStack Start graph.
66 // ---------------------------------------------------------------------------
77
8−export { marketingMiddleware } from "./marketing";
98 export { sentryTunnelMiddleware } from "./sentry-tunnel";
109 export { posthogProxyMiddleware } from "./posthog";
1110 export { docsProxyMiddleware } from "./docs";
apps/cloud/src/edge/marketing.test.ts+52−1
@@ -1,6 +1,6 @@
11 import { describe, expect, it } from "@effect/vitest";
22
3−import { isMarketingPath } from "./marketing";
3+import { isMarketingPath, marketingProxyRequest } from "./marketing";
44
55 // On executor.sh the marketing middleware proxies an allow-list of paths to the
66 // `executor-marketing` worker; everything else falls through to the auth-gated
@@ -38,3 +38,54 @@ describe("isMarketingPath", () => { · 31 unmodified lines
3838 });
3939 }
4040 });
41+
42+describe("marketingProxyRequest", () => {
43+ it("routes a signed-out homepage request", () => {
44+ const request = new Request("https://executor.sh/?source=test");
45+
46+ const proxied = marketingProxyRequest(request);
47+
48+ expect(proxied?.url).toBe("https://executor.sh/?source=test");
49+ });
50+
51+ it("leaves the signed-in homepage with the cloud application", () => {
52+ const request = new Request("https://executor.sh/", {
53+ headers: { cookie: "other=value; wos-session=sealed" },
54+ });
55+
56+ expect(marketingProxyRequest(request)).toBeNull();
57+ });
58+
59+ it("routes public content even when a session cookie is present", () => {
60+ const request = new Request("https://executor.sh/blog/post", {
61+ headers: { cookie: "wos-session=sealed" },
62+ });
63+
64+ expect(marketingProxyRequest(request)?.url).toBe("https://executor.sh/blog/post");
65+ });
66+
67+ it("rewrites the public home alias to the marketing root", () => {
68+ const request = new Request("https://executor.sh/home?source=test");
69+
70+ expect(marketingProxyRequest(request)?.url).toBe("https://executor.sh/?source=test");
71+ });
72+
73+ it("preserves the request method, headers, and body", async () => {
74+ const request = new Request("https://executor.sh/_astro/_ph/capture", {
75+ method: "POST",
76+ headers: { "content-type": "application/json", "x-request-id": "request-1" },
77+ body: JSON.stringify({ event: "test" }),
78+ });
79+
80+ const proxied = marketingProxyRequest(request);
81+
82+ expect(proxied?.method).toBe("POST");
83+ expect(proxied?.headers.get("x-request-id")).toBe("request-1");
84+ await expect(proxied?.json()).resolves.toEqual({ event: "test" });
85+ });
86+
87+ it("does not proxy non-production hosts or app-owned paths", () => {
88+ expect(marketingProxyRequest(new Request("http://executor-cloud.localhost/"))).toBeNull();
89+ expect(marketingProxyRequest(new Request("https://executor.sh/login"))).toBeNull();
90+ });
91+});
apps/cloud/src/edge/marketing.ts+22−34
@@ -3,14 +3,10 @@ · 2 unmodified lines
33 //
44 // On the production domain (`executor.sh`), marketing paths and the
55 // unauthenticated landing page are served by the separate `executor-marketing`
6−// worker (bound as `env.MARKETING`). In local dev that worker isn't running, so
7−// unauthenticated visits fall through to the cloud app's routes (the sign-in
8−// page).
6+// worker. This module deliberately has no TanStack Start or cloud application
7+// imports: the Worker entry calls it before loading the Start server graph.
98 // ---------------------------------------------------------------------------
109
11−import { env } from "cloudflare:workers";
12−import { createMiddleware } from "@tanstack/react-start";
13−
1410 import { parseCookie } from "../auth/cookies";
1511
1612 const MARKETING_PATHS = [
@@ -28,33 +24,25 @@ const MARKETING_PATHS = [ · 11 unmodified lines
2824 "/pattern-graph-paper.svg",
2925 ];
3026
31−export const isMarketingPath = (pathname: string) =>
32− MARKETING_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`));
33−
34−const getMarketingWorker = () => env.MARKETING as { fetch: typeof fetch } | undefined;
35−
36−export const marketingMiddleware = createMiddleware({ type: "request" }).server(
37− async ({ pathname, request, next }) => {
38− // Only proxy to the marketing worker on the production domain. In local
39− // dev we don't run `executor-marketing`, so unauthenticated visits fall
40− // through to the cloud app's routes (which show the sign-in page).
41− const host = new URL(request.url).hostname;
42− if (host !== "executor.sh") return next();
27+const SESSION_COOKIE = "wos-session";
4328
44− const shouldProxyToMarketing =
45− isMarketingPath(pathname) ||
46− (pathname === "/" && !parseCookie(request.headers.get("cookie"), "wos-session"));
47−
48− if (!shouldProxyToMarketing) return next();
49−
50− const marketing = getMarketingWorker();
51− if (!marketing) return next();
29+/** Whether an exact pathname belongs to the public marketing worker. */
30+export const isMarketingPath = (pathname: string): boolean =>
31+ MARKETING_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`));
5232
53− const url = new URL(request.url);
54− // Rewrite /home to / so marketing worker serves its homepage
55− if (pathname === "/home") {
56− url.pathname = "/";
57− }
58− return marketing.fetch(new Request(url, request));
59− },
60−);
33+/**
34+ * Project a production request onto the marketing service-binding request.
35+ * Returns `null` when the cloud application owns the request instead.
36+ */
37+export const marketingProxyRequest = (request: Request): Request | null => {
38+ const url = new URL(request.url);
39+ if (url.hostname !== "executor.sh") return null;
40+
41+ const shouldProxy =
42+ isMarketingPath(url.pathname) ||
43+ (url.pathname === "/" && !parseCookie(request.headers.get("cookie"), SESSION_COOKIE));
44+ if (!shouldProxy) return null;
45+
46+ if (url.pathname === "/home") url.pathname = "/";
47+ return new Request(url, request);
48+};
apps/cloud/src/server.ts+9−0
@@ -12,6 +12,7 @@ import * as Sentry from "@sentry/cloudflare"; · 11 unmodified lines
1212 import handler from "@tanstack/react-start/server-entry";
1313
1414 import { isAppOwnedPath } from "./app-paths";
15+import { marketingProxyRequest } from "./edge/marketing";
1516 import { makeCloudMcpAgentHandler } from "./mcp/agent-handler";
1617 import { classifyMcpPath, prepareMcpOrgScope } from "./mcp/mount";
1718 import { parseTraceparent } from "./mcp/traceparent";
@@ -175,6 +176,14 @@ const mcpAgentHandler = makeCloudMcpAgentHandler({ · 157 unmodified lines
175176
176177 const cloudflareHandler: ExportedHandler<Env> = {
177178 fetch: async (request, env, ctx) => {
179+ // Public pages must not enter TanStack Start: its first-request dynamic
180+ // import loads the entire React + Effect server graph and can take seconds
181+ // on a cold isolate. Classify and service-bind marketing at the Worker
182+ // entry, before telemetry or fetchHandler touches that graph.
183+ const marketingRequest = marketingProxyRequest(request);
184+ const marketing: Fetcher | undefined = env.MARKETING;
185+ if (marketingRequest && marketing) return marketing.fetch(marketingRequest);
186+
178187 // Browser OTLP ingress — before the server span opens: exporter traffic
179188 // must never trace itself (the browser already excludes /v1/traces from
180189 // its own tracing for the same reason).
apps/cloud/src/start.ts+10−12
@@ -10,7 +10,6 @@ import { loginPath } from "./auth/return-to"; · 9 unmodified lines
1010 import { prepareMcpOrgScope } from "./mcp/mount";
1111 import {
1212 docsProxyMiddleware,
13− marketingMiddleware,
1413 openAiAppsChallengeMiddleware,
1514 posthogProxyMiddleware,
1615 sentryTunnelMiddleware,
@@ -89,20 +88,19 @@ const appRequestMiddleware = createMiddleware({ type: "request" }).server( · 72 unmodified lines
8988 },
9089 );
9190
92−// The edge concerns (marketing proxy, docs proxy, sentry tunnel, posthog proxy)
93−// live in `./edge`; they run before the app's own dispatch. Ordering is
94−// load-bearing: marketing first (production landing/page proxy), then the docs
95−// proxy and analytics tunnels, then the unified app plane (api + mcp), and last
96−// the SSR auth gate — it only sees document requests nothing above claimed, so
97−// signed-out visitors are redirected to /login before the SPA (and its
98−// app-shell skeleton) is served. The docs proxy sits among the edges (not after
99−// the auth gate) because `/docs` is public and must skip the sign-in redirect;
100−// its path is disjoint from every other matcher, so its slot is not otherwise
101−// load-bearing.
91+// The remaining edge concerns (docs proxy, sentry tunnel, posthog proxy) live
92+// in `./edge`; they run before the app's own dispatch. Marketing is handled in
93+// server.ts before this module is loaded. Ordering here is load-bearing: public
94+// challenges and docs, then analytics tunnels, then the unified app plane (api
95+// + mcp), and last the SSR auth gate — it only sees document requests nothing
96+// above claimed, so signed-out visitors are redirected to /login before the SPA
97+// (and its app-shell skeleton) is served. The docs proxy sits among the edges
98+// (not after the auth gate) because `/docs` is public and must skip the sign-in
99+// redirect; its path is disjoint from every other matcher, so its slot is not
100+// otherwise load-bearing.
102101 export const startInstance = createStart(() => ({
103102 requestMiddleware: [
104103 openAiAppsChallengeMiddleware,
105− marketingMiddleware,
106104 docsProxyMiddleware,
107105 sentryTunnelMiddleware,
108106 posthogProxyMiddleware,