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 @@
1// ---------------------------------------------------------------------------1// ---------------------------------------------------------------------------
2// Edge concerns — the analytics/marketing/docs request middlewares that run at2// Edge concerns — request middleware that runs before the app's own mcp + api
3// the worker edge BEFORE the app's own mcp + api dispatch. None of these touch3// dispatch. These proxy or tunnel to external services without touching the
4// the Effect app layer; they proxy or tunnel to external services (the4// Effect app layer. Marketing is dispatched even earlier, from server.ts, so a
5// marketing worker, Sentry, PostHog, Mintlify docs).5// public page never loads the TanStack Start graph.
6// ---------------------------------------------------------------------------6// ---------------------------------------------------------------------------
7 7
8export { marketingMiddleware } from "./marketing";
9export { sentryTunnelMiddleware } from "./sentry-tunnel";8export { sentryTunnelMiddleware } from "./sentry-tunnel";
10export { posthogProxyMiddleware } from "./posthog";9export { posthogProxyMiddleware } from "./posthog";
11export { docsProxyMiddleware } from "./docs";10export { docsProxyMiddleware } from "./docs";
apps/cloud/src/edge/marketing.test.ts+52−1
@@ -1,6 +1,6 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2 2
3import { isMarketingPath } from "./marketing";3import { isMarketingPath, marketingProxyRequest } from "./marketing";
4 4
5// On executor.sh the marketing middleware proxies an allow-list of paths to the5// On executor.sh the marketing middleware proxies an allow-list of paths to the
6// `executor-marketing` worker; everything else falls through to the auth-gated6// `executor-marketing` worker; everything else falls through to the auth-gated
@@ -38,3 +38,54 @@ describe("isMarketingPath", () => { · 31 unmodified lines
38 });38 });
39 }39 }
40});40});
41
42describe("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
3//3//
4// On the production domain (`executor.sh`), marketing paths and the4// On the production domain (`executor.sh`), marketing paths and the
5// unauthenticated landing page are served by the separate `executor-marketing`5// unauthenticated landing page are served by the separate `executor-marketing`
6// worker (bound as `env.MARKETING`). In local dev that worker isn't running, so6// worker. This module deliberately has no TanStack Start or cloud application
7// unauthenticated visits fall through to the cloud app's routes (the sign-in7// imports: the Worker entry calls it before loading the Start server graph.
8// page).
9// ---------------------------------------------------------------------------8// ---------------------------------------------------------------------------
10 9
11import { env } from "cloudflare:workers";
12import { createMiddleware } from "@tanstack/react-start";
13
14import { parseCookie } from "../auth/cookies";10import { parseCookie } from "../auth/cookies";
15 11
16const MARKETING_PATHS = [12const MARKETING_PATHS = [
@@ -28,33 +24,25 @@ const MARKETING_PATHS = [ · 11 unmodified lines
28 "/pattern-graph-paper.svg",24 "/pattern-graph-paper.svg",
29];25];
30 26
31export const isMarketingPath = (pathname: string) =>27const SESSION_COOKIE = "wos-session";
32 MARKETING_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`));
33
34const getMarketingWorker = () => env.MARKETING as { fetch: typeof fetch } | undefined;
35
36export 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();
43 28
44 const shouldProxyToMarketing =29/** Whether an exact pathname belongs to the public marketing worker. */
45 isMarketingPath(pathname) ||30export const isMarketingPath = (pathname: string): boolean =>
46 (pathname === "/" && !parseCookie(request.headers.get("cookie"), "wos-session"));31 MARKETING_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`));
47
48 if (!shouldProxyToMarketing) return next();
49
50 const marketing = getMarketingWorker();
51 if (!marketing) return next();
52 32
53 const url = new URL(request.url);33/**
54 // Rewrite /home to / so marketing worker serves its homepage34 * Project a production request onto the marketing service-binding request.
55 if (pathname === "/home") {35 * Returns `null` when the cloud application owns the request instead.
56 url.pathname = "/";36 */
57 }37export const marketingProxyRequest = (request: Request): Request | null => {
58 return marketing.fetch(new Request(url, request));38 const url = new URL(request.url);
59 },39 if (url.hostname !== "executor.sh") return null;
60);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
12import handler from "@tanstack/react-start/server-entry";12import handler from "@tanstack/react-start/server-entry";
13 13
14import { isAppOwnedPath } from "./app-paths";14import { isAppOwnedPath } from "./app-paths";
15import { marketingProxyRequest } from "./edge/marketing";
15import { makeCloudMcpAgentHandler } from "./mcp/agent-handler";16import { makeCloudMcpAgentHandler } from "./mcp/agent-handler";
16import { classifyMcpPath, prepareMcpOrgScope } from "./mcp/mount";17import { classifyMcpPath, prepareMcpOrgScope } from "./mcp/mount";
17import { parseTraceparent } from "./mcp/traceparent";18import { parseTraceparent } from "./mcp/traceparent";
@@ -175,6 +176,14 @@ const mcpAgentHandler = makeCloudMcpAgentHandler({ · 157 unmodified lines
175 176
176const cloudflareHandler: ExportedHandler<Env> = {177const cloudflareHandler: ExportedHandler<Env> = {
177 fetch: async (request, env, ctx) => {178 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
178 // Browser OTLP ingress — before the server span opens: exporter traffic187 // Browser OTLP ingress — before the server span opens: exporter traffic
179 // must never trace itself (the browser already excludes /v1/traces from188 // must never trace itself (the browser already excludes /v1/traces from
180 // its own tracing for the same reason).189 // 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
10import { prepareMcpOrgScope } from "./mcp/mount";10import { prepareMcpOrgScope } from "./mcp/mount";
11import {11import {
12 docsProxyMiddleware,12 docsProxyMiddleware,
13 marketingMiddleware,
14 openAiAppsChallengeMiddleware,13 openAiAppsChallengeMiddleware,
15 posthogProxyMiddleware,14 posthogProxyMiddleware,
16 sentryTunnelMiddleware,15 sentryTunnelMiddleware,
@@ -89,20 +88,19 @@ const appRequestMiddleware = createMiddleware({ type: "request" }).server( · 72 unmodified lines
89 },88 },
90);89);
91 90
92// The edge concerns (marketing proxy, docs proxy, sentry tunnel, posthog proxy)91// The remaining edge concerns (docs proxy, sentry tunnel, posthog proxy) live
93// live in `./edge`; they run before the app's own dispatch. Ordering is92// in `./edge`; they run before the app's own dispatch. Marketing is handled in
94// load-bearing: marketing first (production landing/page proxy), then the docs93// server.ts before this module is loaded. Ordering here is load-bearing: public
95// proxy and analytics tunnels, then the unified app plane (api + mcp), and last94// challenges and docs, then analytics tunnels, then the unified app plane (api
96// the SSR auth gate — it only sees document requests nothing above claimed, so95// + mcp), and last the SSR auth gate — it only sees document requests nothing
97// signed-out visitors are redirected to /login before the SPA (and its96// above claimed, so signed-out visitors are redirected to /login before the SPA
98// app-shell skeleton) is served. The docs proxy sits among the edges (not after97// (and its app-shell skeleton) is served. The docs proxy sits among the edges
99// the auth gate) because `/docs` is public and must skip the sign-in redirect;98// (not after the auth gate) because `/docs` is public and must skip the sign-in
100// its path is disjoint from every other matcher, so its slot is not otherwise99// redirect; its path is disjoint from every other matcher, so its slot is not
101// load-bearing.100// otherwise load-bearing.
102export const startInstance = createStart(() => ({101export const startInstance = createStart(() => ({
103 requestMiddleware: [102 requestMiddleware: [
104 openAiAppsChallengeMiddleware,103 openAiAppsChallengeMiddleware,
105 marketingMiddleware,
106 docsProxyMiddleware,104 docsProxyMiddleware,
107 sentryTunnelMiddleware,105 sentryTunnelMiddleware,
108 posthogProxyMiddleware,106 posthogProxyMiddleware,