Unified · Split
| @@ -1,11 +1,10 @@ | |||
| 1 | // --------------------------------------------------------------------------- | 1 | // --------------------------------------------------------------------------- |
| 2 | // Edge concerns — the analytics/marketing/docs request middlewares that run at | 2 | // 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 touch | 3 | // dispatch. These proxy or tunnel to external services without touching the |
| 4 | // the Effect app layer; they proxy or tunnel to external services (the | 4 | // 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 | ||
| 8 | export { marketingMiddleware } from "./marketing"; | ||
| 9 | export { sentryTunnelMiddleware } from "./sentry-tunnel"; | 8 | export { sentryTunnelMiddleware } from "./sentry-tunnel"; |
| 10 | export { posthogProxyMiddleware } from "./posthog"; | 9 | export { posthogProxyMiddleware } from "./posthog"; |
| 11 | export { docsProxyMiddleware } from "./docs"; | 10 | export { docsProxyMiddleware } from "./docs"; |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | ||
| 3 | import { isMarketingPath } from "./marketing"; | 3 | import { isMarketingPath, marketingProxyRequest } from "./marketing"; |
| 4 | 4 | ||
| 5 | // On executor.sh the marketing middleware proxies an allow-list of paths to the | 5 | // 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-gated | 6 | // `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 | |||
| 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 | }); | ||
| @@ -3,14 +3,10 @@ · 2 unmodified lines | |||
| 3 | // | 3 | // |
| 4 | // On the production domain (`executor.sh`), marketing paths and the | 4 | // 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, so | 6 | // worker. This module deliberately has no TanStack Start or cloud application |
| 7 | // unauthenticated visits fall through to the cloud app's routes (the sign-in | 7 | // imports: the Worker entry calls it before loading the Start server graph. |
| 8 | // page). | ||
| 9 | // --------------------------------------------------------------------------- | 8 | // --------------------------------------------------------------------------- |
| 10 | 9 | ||
| 11 | import { env } from "cloudflare:workers"; | ||
| 12 | import { createMiddleware } from "@tanstack/react-start"; | ||
| 13 | |||
| 14 | import { parseCookie } from "../auth/cookies"; | 10 | import { parseCookie } from "../auth/cookies"; |
| 15 | 11 | ||
| 16 | const MARKETING_PATHS = [ | 12 | const 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 | ||
| 31 | export const isMarketingPath = (pathname: string) => | 27 | const SESSION_COOKIE = "wos-session"; |
| 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(); | ||
| 43 | 28 | ||
| 44 | const shouldProxyToMarketing = | 29 | /** Whether an exact pathname belongs to the public marketing worker. */ |
| 45 | isMarketingPath(pathname) || | 30 | export 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 homepage | 34 | * 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 | } | 37 | export 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 | }; | ||
| @@ -12,6 +12,7 @@ import * as Sentry from "@sentry/cloudflare"; · 11 unmodified lines | |||
| 12 | import handler from "@tanstack/react-start/server-entry"; | 12 | import handler from "@tanstack/react-start/server-entry"; |
| 13 | 13 | ||
| 14 | import { isAppOwnedPath } from "./app-paths"; | 14 | import { isAppOwnedPath } from "./app-paths"; |
| 15 | import { marketingProxyRequest } from "./edge/marketing"; | ||
| 15 | import { makeCloudMcpAgentHandler } from "./mcp/agent-handler"; | 16 | import { makeCloudMcpAgentHandler } from "./mcp/agent-handler"; |
| 16 | import { classifyMcpPath, prepareMcpOrgScope } from "./mcp/mount"; | 17 | import { classifyMcpPath, prepareMcpOrgScope } from "./mcp/mount"; |
| 17 | import { parseTraceparent } from "./mcp/traceparent"; | 18 | import { parseTraceparent } from "./mcp/traceparent"; |
| @@ -175,6 +176,14 @@ const mcpAgentHandler = makeCloudMcpAgentHandler({ · 157 unmodified lines | |||
| 175 | 176 | ||
| 176 | const cloudflareHandler: ExportedHandler<Env> = { | 177 | const 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 traffic | 187 | // Browser OTLP ingress — before the server span opens: exporter traffic |
| 179 | // must never trace itself (the browser already excludes /v1/traces from | 188 | // 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). |
| @@ -10,7 +10,6 @@ import { loginPath } from "./auth/return-to"; · 9 unmodified lines | |||
| 10 | import { prepareMcpOrgScope } from "./mcp/mount"; | 10 | import { prepareMcpOrgScope } from "./mcp/mount"; |
| 11 | import { | 11 | import { |
| 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 is | 92 | // 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 docs | 93 | // 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 last | 94 | // 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, so | 95 | // + 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 its | 96 | // 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 after | 97 | // (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 otherwise | 99 | // redirect; its path is disjoint from every other matcher, so its slot is not |
| 101 | // load-bearing. | 100 | // otherwise load-bearing. |
| 102 | export const startInstance = createStart(() => ({ | 101 | export 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, |