From fbbe315a1c1b4ac7927307c0fae78e7e190dedcb Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Thu, 8 Oct 2026 01:48:07 -0700 Subject: [PATCH 1/5] Serve executor.sh marketing from v2 and keep v1's terms --- apps/cloud/src/edge/marketing.ts | 181 +++++++++++++++++------- apps/cloud/src/env-augment.d.ts | 3 + apps/cloud/src/server.ts | 7 +- apps/cloud/wrangler.jsonc | 12 +- apps/marketing/astro.config.mjs | 4 + apps/marketing/src/layouts/Layout.astro | 7 +- apps/marketing/src/middleware.ts | 7 +- 7 files changed, 156 insertions(+), 65 deletions(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index 31942b1cae..2bbcfe4d29 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -2,14 +2,16 @@ // The `executor.sh` edge — decides which Worker answers a public request. // // On the production domain (`executor.sh`): -// - marketing paths and the unauthenticated landing page go to the separate -// `executor-marketing` worker; +// - v2's marketing site answers the landing page without a v1 session, its +// pages, assets and docs, and its browser telemetry proxies; // - sign-up goes to v2 (a redirect to v2's sign-up page); // - a fixed list of v2 paths (sign-in issuer metadata, social sign-in // callbacks, Git smart HTTP and the agent skills index) is forwarded to // v2's Worker over a service binding; // - so is a connected-account OAuth callback whose `state` carries v2's -// prefix. +// prefix; +// - v1's terms of service (and their assets) stay on the +// `executor-marketing` worker. // v1 owns everything not listed. This module deliberately has no TanStack // Start or cloud application imports: the Worker entry calls it before // loading the Start server graph. @@ -17,49 +19,33 @@ import { parseCookie } from "../auth/cookies"; -const MARKETING_PATHS = [ - "/home", - "/setup", - "/privacy", - "/terms", - "/pricing", - "/about-executor", - "/google-oauth", - "/google-workspace", - "/blog", - "/llms.txt", - "/index.md", - "/setup-prompt.md", - "/pricing.md", - "/api/detect", - "/_astro", - "/authors", - "/og-image.png", - "/pattern-graph-paper.svg", -]; +const PRODUCTION_HOST = "executor.sh"; +/** v1's session cookie. `/` with it is v1's dashboard; without it, marketing. */ const SESSION_COOKIE = "wos-session"; -const PRODUCTION_HOST = "executor.sh"; +// --------------------------------------------------------------------------- +// v1's terms on the `executor-marketing` worker +// --------------------------------------------------------------------------- + +/** v1's terms of service stay on v1's marketing worker: v2's terms describe + * only v2's billing, and v1's cover the plans v1 customers are on. The + * worker builds its assets under `/_v1-marketing`, apart from v2's + * `/_astro`. Each path matches itself and everything below it. */ +const V1_MARKETING_PATHS: ReadonlyArray = ["/terms", "/_v1-marketing"]; -/** Whether an exact pathname belongs to the public marketing worker. */ +/** Whether a pathname belongs to v1's marketing worker on `executor.sh`. */ export const isMarketingPath = (pathname: string): boolean => - MARKETING_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`)); + V1_MARKETING_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`)); /** - * Project a production request onto the marketing service-binding request. - * Returns `null` when the cloud application owns the request instead. + * Project a production request for v1's terms (or their assets) onto the + * `executor-marketing` service-binding request. Returns `null` for every + * other request. */ export const marketingProxyRequest = (request: Request): Request | null => { const url = new URL(request.url); - if (url.hostname !== PRODUCTION_HOST) return null; - - const shouldProxy = - isMarketingPath(url.pathname) || - (url.pathname === "/" && !parseCookie(request.headers.get("cookie"), SESSION_COOKIE)); - if (!shouldProxy) return null; - - if (url.pathname === "/home") url.pathname = "/"; + if (url.hostname !== PRODUCTION_HOST || !isMarketingPath(url.pathname)) return null; return new Request(url, request); }; @@ -103,6 +89,51 @@ export const isV2Path = (pathname: string): boolean => V2_PATHS.some((pattern) => matchesPathPattern(pathname, pattern)) || isSocialCallbackPath(pathname); +/** v2's marketing site on `executor.sh`: each path and everything below it. + * These are the pages, files and assets v2's marketing build publishes, and + * its docs. Paths that are also valid v1 organization slugs and not + * reserved (v2's `/apps`, `/demo` and `/experiments`) stay with v1. The + * favicons stay with v1 too: v1's dashboard serves identical files. */ +const V2_MARKETING_PATHS: ReadonlyArray = [ + "/home", + "/about-executor", + "/blog", + "/pricing", + "/privacy", + "/google-oauth", + "/google-workspace", + "/index.md", + "/llms.txt", + "/pricing.md", + "/setup-prompt.md", + "/_astro", + "/authors", + "/og-image.png", + "/pattern-graph-paper.svg", + "/docs", +]; + +/** Whether a pathname belongs to v2's marketing site on `executor.sh`. `/` + * also does, without a v1 session; see {@link isV2Homepage}. */ +export const isV2MarketingPath = (pathname: string): boolean => + V2_MARKETING_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`)); + +/** The landing page without a v1 session is v2's marketing homepage. */ +const isV2Homepage = (url: URL, request: Request): boolean => + url.pathname === "/" && parseCookie(request.headers.get("cookie"), SESSION_COOKIE) === null; + +/** The one cookie v2's marketing reads: the anonymous visitor ID that keeps + * a visitor's homepage experiment assignment stable. v2 sets it on + * `executor.sh` itself. Every other cookie, v1's session included, stays + * with v1. */ +const V2_MARKETING_COOKIES: ReadonlyArray = ["executor_visitor"]; + +/** v2's browser telemetry proxies (product analytics and error reporting) + * live at `/api/<16 hex>` roots, which v1's API never uses. The configured + * roots (`V2_TELEMETRY_PATHS`) say which ones are v2's. */ +const TELEMETRY_ROOT_PATTERN = /^\/api\/[a-f0-9]{16}$/; +const TELEMETRY_PATH_SHAPE = /^\/api\/[a-f0-9]{16}(?:\/|$)/; + /** Request headers v1 never passes to v2. The cookie header carries v1's * `wos-session` (v2's cookies are host-only on its own hosts, so nothing of * v2's travels on `executor.sh`). Client-sent forwarding headers are dropped @@ -114,12 +145,22 @@ const V2_STRIPPED_HEADERS = ["cookie", "x-forwarded-host", "x-forwarded-proto"] * * Keeps the URL (so v2 sees host `executor.sh`, path and query unchanged), * method, body stream and every header except {@link V2_STRIPPED_HEADERS}, - * including `Authorization`. Redirects are returned to the client, not + * including `Authorization`. The cookie header is rebuilt from `keptCookies` + * only, and dropped when none of them is present. Redirects are returned to the client, not * followed: v2 answers callbacks with a redirect to its own host. */ -export const v2ForwardRequest = (request: Request): Request => { +export const v2ForwardRequest = ( + request: Request, + keptCookies: ReadonlyArray = [], +): Request => { + const cookieHeader = request.headers.get("cookie"); + const cookies = keptCookies.flatMap((name) => { + const value = parseCookie(cookieHeader, name); + return value === null ? [] : [`${name}=${value}`]; + }); const headers = new Headers(request.headers); for (const name of V2_STRIPPED_HEADERS) headers.delete(name); + if (cookies.length > 0) headers.set("cookie", cookies.join("; ")); return new Request(request, { headers, redirect: "manual" }); }; @@ -145,6 +186,8 @@ export interface V2EdgeEnv { readonly V2_SIGN_UP_URL?: string; /** The prefix v2 puts on every connected-account OAuth `state`. */ readonly V2_OAUTH_STATE_PREFIX?: string; + /** Comma-separated `/api/<16 hex>` roots of v2's browser telemetry proxies. */ + readonly V2_TELEMETRY_PATHS?: string; } /** What the edge needs to hand requests to v2. */ @@ -155,24 +198,37 @@ export interface V2Edge { readonly signUpUrl: URL; /** v2's connected-account OAuth state prefix (the `V2_OAUTH_STATE_PREFIX` var). */ readonly oauthStatePrefix: string; + /** Roots of v2's browser telemetry proxies (the `V2_TELEMETRY_PATHS` var). */ + readonly telemetryPaths: ReadonlyArray; } /** * Parse the edge's v2 settings from the Worker environment. Returns `null` * when none is set (local dev, test workers), so v1 serves everything, and * the reason as a string when the deployment is broken: only some of them - * set, a sign-up URL that is not absolute, or a state prefix that could - * match a v1 state. + * set, a sign-up URL that is not absolute, a state prefix that could match a + * v1 state, or a telemetry root outside `/api/<16 hex>`. */ export const parseV2Edge = (env: V2EdgeEnv): V2Edge | string | null => { const service = env.V2; const signUpUrl = env.V2_SIGN_UP_URL; const oauthStatePrefix = env.V2_OAUTH_STATE_PREFIX; - if (service === undefined && signUpUrl === undefined && oauthStatePrefix === undefined) { + const telemetryPaths = env.V2_TELEMETRY_PATHS; + if ( + service === undefined && + signUpUrl === undefined && + oauthStatePrefix === undefined && + telemetryPaths === undefined + ) { return null; } - if (service === undefined || signUpUrl === undefined || oauthStatePrefix === undefined) { - return "The V2 binding, V2_SIGN_UP_URL and V2_OAUTH_STATE_PREFIX must be set together"; + if ( + service === undefined || + signUpUrl === undefined || + oauthStatePrefix === undefined || + telemetryPaths === undefined + ) { + return "The V2 binding, V2_SIGN_UP_URL, V2_OAUTH_STATE_PREFIX and V2_TELEMETRY_PATHS must be set together"; } const parsed = URL.parse(signUpUrl); if (parsed === null || (parsed.protocol !== "https:" && parsed.protocol !== "http:")) { @@ -181,7 +237,11 @@ export const parseV2Edge = (env: V2EdgeEnv): V2Edge | string | null => { if (!OAUTH_STATE_PREFIX_PATTERN.test(oauthStatePrefix)) { return "V2_OAUTH_STATE_PREFIX must be URL-safe and contain '.' or '~'"; } - return { service, signUpUrl: parsed, oauthStatePrefix }; + const roots = telemetryPaths.split(",").map((path) => path.trim()); + if (!roots.every((root) => TELEMETRY_ROOT_PATTERN.test(root))) { + return "V2_TELEMETRY_PATHS must list /api/<16 hex> paths separated by commas"; + } + return { service, signUpUrl: parsed, oauthStatePrefix, telemetryPaths: roots }; }; /** Whether a connected-account callback carries v2's state prefix. Reads the @@ -189,32 +249,45 @@ export const parseV2Edge = (env: V2EdgeEnv): V2Edge | string | null => { const isV2OAuthCallback = (url: URL, prefix: string): boolean => url.searchParams.get("state")?.startsWith(prefix) === true; +/** Whether a pathname is under one of v2's telemetry proxy roots. */ +const isV2TelemetryPath = (pathname: string, roots: ReadonlyArray): boolean => + roots.some((root) => pathname === root || pathname.startsWith(`${root}/`)); + /** - * Answer a production request that belongs to v2: redirect sign-up (`GET`, - * any query) to v2's sign-up page, or forward a {@link isV2Path} request, or - * a connected-account callback whose `state` starts with v2's prefix, to - * v2's Worker and return its response unchanged (status, headers and body - * stream). Returns `null` when v1 owns the request. + * Answer a production request that belongs to v2 and return `null` when v1 + * owns it: + * - sign-up (`GET`, any query) redirects to v2's sign-up page; + * - a {@link isV2Path} request, a connected-account callback whose `state` + * starts with v2's prefix, or a request under one of v2's telemetry roots + * is forwarded to v2's Worker without cookies; + * - a {@link isV2MarketingPath} request, or `/` without a v1 session, is + * forwarded with only v2's marketing visitor cookie. + * v2's response comes back unchanged (status, headers and body stream). * - * Broken settings answer the requests the edge owns with a 500 and leave the - * rest of v1 serving. The callback is the exception: which callbacks are v2's - * depends on the settings, so v1 keeps every callback until they parse. + * Broken settings answer the requests the edge always owns with a 500 and + * leave the rest of v1 serving. Which callbacks and `/api/<16 hex>` requests + * are v2's depends on the settings, so v1 keeps those until they parse. */ export const v2EdgeResponse = (request: Request, env: V2EdgeEnv): Promise | null => { const url = new URL(request.url); if (url.hostname !== PRODUCTION_HOST) return null; const signUp = isSignUpPath(url.pathname) && request.method === "GET"; + const marketing = isV2MarketingPath(url.pathname) || isV2Homepage(url, request); + const owned = signUp || marketing || isV2Path(url.pathname); const callback = url.pathname === OAUTH_CALLBACK_PATH; - if (!signUp && !callback && !isV2Path(url.pathname)) return null; + const telemetry = TELEMETRY_PATH_SHAPE.test(url.pathname); + if (!owned && !callback && !telemetry) return null; const edge = parseV2Edge(env); if (edge === null) return null; if (typeof edge === "string") { + if (!owned) return null; console.error(`executor.sh v2 edge misconfigured: ${edge}`); - if (callback) return null; return Promise.resolve(new Response("Service misconfigured", { status: 500 })); } if (signUp) return Promise.resolve(Response.redirect(edge.signUpUrl.href, 302)); + if (marketing) return edge.service.fetch(v2ForwardRequest(request, V2_MARKETING_COOKIES)); if (callback && !isV2OAuthCallback(url, edge.oauthStatePrefix)) return null; + if (telemetry && !isV2TelemetryPath(url.pathname, edge.telemetryPaths)) return null; return edge.service.fetch(v2ForwardRequest(request)); }; diff --git a/apps/cloud/src/env-augment.d.ts b/apps/cloud/src/env-augment.d.ts index e733d119fe..b0d684206a 100644 --- a/apps/cloud/src/env-augment.d.ts +++ b/apps/cloud/src/env-augment.d.ts @@ -146,6 +146,9 @@ declare global { /** Prefix of v2's connected-account OAuth `state`; `/api/oauth/callback` * requests whose state starts with it go to v2. */ V2_OAUTH_STATE_PREFIX?: string; + /** Comma-separated `/api/<16 hex>` roots of v2's browser telemetry + * proxies, which v2's marketing pages call on executor.sh. */ + V2_TELEMETRY_PATHS?: string; // Shared with frontend VITE_PUBLIC_SITE_URL?: string; diff --git a/apps/cloud/src/server.ts b/apps/cloud/src/server.ts index 1a1a713c2d..6faff79d3b 100644 --- a/apps/cloud/src/server.ts +++ b/apps/cloud/src/server.ts @@ -318,8 +318,9 @@ const cloudflareHandler = { prewarmAppPlane(ctx); } - // Sign-up, the fixed list of v2 paths and v2's connected-account - // callbacks on `executor.sh` go to v2. + // On `executor.sh`, v2 answers marketing (including `/docs` and its + // telemetry proxies), sign-up, the fixed list of v2 paths and v2's + // connected-account callbacks; v1's terms stay on v1's marketing worker. const v2 = v2EdgeResponse(request, env); if (v2) return v2; @@ -333,6 +334,8 @@ const cloudflareHandler = { // first — measured at p50 3.1s on a cold isolate, against p50 33ms for the // request's own work, on a Worker where 1,666 dispatches spread across // 1,608 isolates (so nearly every request is cold). Forward before Start. + // On `executor.sh` v2 answers `/docs` above, so this serves it on other + // hosts; v1's own PostHog proxy is served here on every host. const passthroughPath = new URL(request.url).pathname; const passthrough = passthroughResponse(request, passthroughPath); if (passthrough) return passthrough; diff --git a/apps/cloud/wrangler.jsonc b/apps/cloud/wrangler.jsonc index 95aa2eca4f..12119057fe 100644 --- a/apps/cloud/wrangler.jsonc +++ b/apps/cloud/wrangler.jsonc @@ -91,13 +91,17 @@ }, ], "services": [ + // v1's marketing worker. On executor.sh it now serves only v1's terms of + // service and their assets (src/edge/marketing.ts); v2 serves the rest + // of marketing. { "binding": "MARKETING", "service": "executor-marketing", }, // v2's API Worker (the executor-next `v2` stage, same account). The edge - // (src/edge/marketing.ts) forwards a fixed list of executor.sh paths to - // it with the URL unchanged, so v2 sees host `executor.sh`. + // (src/edge/marketing.ts) forwards marketing and a fixed list of + // executor.sh paths to it with the URL unchanged, so v2 sees host + // `executor.sh`. { "binding": "V2", // v2's Worker name. It must equal the name v2's deployment pins for @@ -148,6 +152,10 @@ // /api/oauth/callback requests carrying it go to v2. It must equal the // prefix v2 mints. "V2_OAUTH_STATE_PREFIX": "x2.", + // Roots of v2's browser telemetry proxies (product analytics, then error + // reporting) that v2's marketing pages call on executor.sh. They must + // equal the proxy paths v2's build uses. + "V2_TELEMETRY_PATHS": "/api/00e2e1f082a6ef17,/api/fd6fab1fbb4883e1", // Keeps the /__sentry-otel-verify probe live in production: Sentry // delivery failed silently for weeks (zero events after ~Jul 30 with an // active DSN), and without this there is no way to test the pipeline diff --git a/apps/marketing/astro.config.mjs b/apps/marketing/astro.config.mjs index 877af5b812..4537b81615 100644 --- a/apps/marketing/astro.config.mjs +++ b/apps/marketing/astro.config.mjs @@ -30,6 +30,10 @@ export default defineConfig({ site: "https://executor.sh", output: "server", integrations: [react()], + // On executor.sh this worker now serves only v1's terms of service; v2's + // marketing site owns `/_astro`. Build assets under their own root, which + // the cloud edge (apps/cloud/src/edge/marketing.ts) forwards here. + build: { assets: "_v1-marketing" }, vite: { plugins: [tailwindcss()], define: wranglerPublicDefine(), diff --git a/apps/marketing/src/layouts/Layout.astro b/apps/marketing/src/layouts/Layout.astro index d791ecda8c..35b4f689a1 100644 --- a/apps/marketing/src/layouts/Layout.astro +++ b/apps/marketing/src/layouts/Layout.astro @@ -62,11 +62,12 @@ const canonical = new URL(Astro.url.pathname, Astro.site ?? Astro.url).toString( // `history_change` is for SPAs like apps/cloud. const phKey = import.meta.env.PUBLIC_POSTHOG_KEY; if (phKey) { - // api_host rides the `/_astro` prefix that the cloud edge forwards to - // this worker on executor.sh; src/middleware.ts proxies it to PostHog. + // api_host rides the `/_v1-marketing` asset root that the cloud edge + // forwards to this worker on executor.sh; src/middleware.ts proxies it + // to PostHog. const posthogReady = import("posthog-js").then(({ default: posthog }) => { posthog.init(phKey, { - api_host: `${window.location.origin}/_astro/_ph`, + api_host: `${window.location.origin}/_v1-marketing/_ph`, ui_host: import.meta.env.PUBLIC_POSTHOG_HOST ?? "https://us.posthog.com", autocapture: false, capture_pageview: true, diff --git a/apps/marketing/src/middleware.ts b/apps/marketing/src/middleware.ts index dbbd339e7f..b676eba7af 100644 --- a/apps/marketing/src/middleware.ts +++ b/apps/marketing/src/middleware.ts @@ -7,12 +7,11 @@ import type { MiddlewareHandler } from "astro"; // // The path MUST sit under a prefix that the cloud worker's edge forwards to // this worker. On the production apex (executor.sh) the cloud worker owns the -// custom domain and only proxies an allow-list to executor-marketing; `/api/*` -// stays in cloud (it has its own posthog proxy on a randomized path), so a -// top-level `/api/...` path here 404s. `/_astro` IS forwarded, so we ride it. +// custom domain and only proxies `/terms` and the `/_v1-marketing` asset root +// to executor-marketing, so we ride the asset root. const POSTHOG_INGEST_HOST = "us.i.posthog.com"; const POSTHOG_ASSETS_HOST = "us-assets.i.posthog.com"; -const POSTHOG_PROXY_PATH = "/_astro/_ph"; +const POSTHOG_PROXY_PATH = "/_v1-marketing/_ph"; export const onRequest: MiddlewareHandler = async (context, next) => { const { pathname } = new URL(context.request.url); From e4236bb68e191127c3d40eb6252d46d02105fcc3 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Thu, 8 Oct 2026 01:54:18 -0700 Subject: [PATCH 2/5] Describe the visitor cookie the edge passes to v2 --- apps/cloud/src/edge/marketing.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index 2bbcfe4d29..8db8b7d2ff 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -134,10 +134,11 @@ const V2_MARKETING_COOKIES: ReadonlyArray = ["executor_visitor"]; const TELEMETRY_ROOT_PATTERN = /^\/api\/[a-f0-9]{16}$/; const TELEMETRY_PATH_SHAPE = /^\/api\/[a-f0-9]{16}(?:\/|$)/; -/** Request headers v1 never passes to v2. The cookie header carries v1's - * `wos-session` (v2's cookies are host-only on its own hosts, so nothing of - * v2's travels on `executor.sh`). Client-sent forwarding headers are dropped - * so v2 sees no host claim other than the request URL's. */ +/** Request headers v1 never passes to v2 as sent. The cookie header carries + * v1's `wos-session`; v2's own cookies are host-only on its own hosts, + * except marketing's visitor cookie, which is passed on by name. Client-sent + * forwarding headers are dropped so v2 sees no host claim other than the + * request URL's. */ const V2_STRIPPED_HEADERS = ["cookie", "x-forwarded-host", "x-forwarded-proto"] as const; /** @@ -146,8 +147,9 @@ const V2_STRIPPED_HEADERS = ["cookie", "x-forwarded-host", "x-forwarded-proto"] * Keeps the URL (so v2 sees host `executor.sh`, path and query unchanged), * method, body stream and every header except {@link V2_STRIPPED_HEADERS}, * including `Authorization`. The cookie header is rebuilt from `keptCookies` - * only, and dropped when none of them is present. Redirects are returned to the client, not - * followed: v2 answers callbacks with a redirect to its own host. + * only, and dropped when none of them is present. Redirects are returned to + * the client, not followed: v2 answers callbacks with a redirect to its own + * host. */ export const v2ForwardRequest = ( request: Request, From 2facf95583de2c3ec8f391b7f1044a379d0aaae6 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:35:42 -0700 Subject: [PATCH 3/5] Keep v1's privacy and Google OAuth pages, match v2's contract and pin its telemetry paths --- apps/cloud/src/edge/marketing.ts | 128 +++++++++++++++++++------------ apps/cloud/src/env-augment.d.ts | 9 ++- apps/cloud/src/server.ts | 3 +- apps/cloud/wrangler.jsonc | 21 +++-- apps/marketing/astro.config.mjs | 2 +- apps/marketing/src/middleware.ts | 4 +- 6 files changed, 107 insertions(+), 60 deletions(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index 8db8b7d2ff..e7a464de44 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -10,8 +10,8 @@ // v2's Worker over a service binding; // - so is a connected-account OAuth callback whose `state` carries v2's // prefix; -// - v1's terms of service (and their assets) stay on the -// `executor-marketing` worker. +// - v1's terms of service, privacy policy and Google OAuth disclosure (and +// their assets) stay on the `executor-marketing` worker. // v1 owns everything not listed. This module deliberately has no TanStack // Start or cloud application imports: the Worker entry calls it before // loading the Start server graph. @@ -25,21 +25,28 @@ const PRODUCTION_HOST = "executor.sh"; const SESSION_COOKIE = "wos-session"; // --------------------------------------------------------------------------- -// v1's terms on the `executor-marketing` worker +// v1's legal pages on the `executor-marketing` worker // --------------------------------------------------------------------------- -/** v1's terms of service stay on v1's marketing worker: v2's terms describe - * only v2's billing, and v1's cover the plans v1 customers are on. The - * worker builds its assets under `/_v1-marketing`, apart from v2's - * `/_astro`. Each path matches itself and everything below it. */ -const V1_MARKETING_PATHS: ReadonlyArray = ["/terms", "/_v1-marketing"]; +/** v1's legal pages stay on v1's marketing worker. v2's terms describe only + * v2's billing, and v1's cover the plans v1 customers are on; v1's privacy + * policy and Google OAuth disclosure describe v1's own data handling and + * Google OAuth app. The worker builds its assets under `/_v1-marketing`, + * apart from v2's `/_astro`. Each path matches itself and everything below + * it. */ +const V1_MARKETING_PATHS: ReadonlyArray = [ + "/terms", + "/privacy", + "/google-oauth", + "/_v1-marketing", +]; /** Whether a pathname belongs to v1's marketing worker on `executor.sh`. */ export const isMarketingPath = (pathname: string): boolean => V1_MARKETING_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`)); /** - * Project a production request for v1's terms (or their assets) onto the + * Project a production request for v1's legal pages (or their assets) onto the * `executor-marketing` service-binding request. Returns `null` for every * other request. */ @@ -89,34 +96,40 @@ export const isV2Path = (pathname: string): boolean => V2_PATHS.some((pattern) => matchesPathPattern(pathname, pattern)) || isSocialCallbackPath(pathname); -/** v2's marketing site on `executor.sh`: each path and everything below it. - * These are the pages, files and assets v2's marketing build publishes, and - * its docs. Paths that are also valid v1 organization slugs and not - * reserved (v2's `/apps`, `/demo` and `/experiments`) stay with v1. The +/** v2's marketing site on `executor.sh`, as path patterns (see + * {@link matchesPathPattern}): the pages, files and assets v2's marketing + * build publishes, and its docs. `/pricing` and `/home` match only + * themselves. `apps` and `experiments` are reserved org slugs, so `/apps`, + * `/apps/*` and `/experiments/*` are v2's; v2 publishes nothing at a bare + * `/experiments`, which stays with v1. `/demo` is a valid, unreserved v1 + * organization slug, so it and everything below it stay with v1. The * favicons stay with v1 too: v1's dashboard serves identical files. */ const V2_MARKETING_PATHS: ReadonlyArray = [ "/home", - "/about-executor", - "/blog", "/pricing", - "/privacy", - "/google-oauth", - "/google-workspace", - "/index.md", - "/llms.txt", "/pricing.md", + "/index.md", "/setup-prompt.md", - "/_astro", - "/authors", + "/llms.txt", + "/about-executor", + "/google-workspace", + "/blog", + "/blog/*", + "/docs", + "/docs/*", + "/apps", + "/apps/*", + "/experiments/*", + "/_astro/*", + "/authors/*", "/og-image.png", "/pattern-graph-paper.svg", - "/docs", ]; /** Whether a pathname belongs to v2's marketing site on `executor.sh`. `/` * also does, without a v1 session; see {@link isV2Homepage}. */ export const isV2MarketingPath = (pathname: string): boolean => - V2_MARKETING_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`)); + V2_MARKETING_PATHS.some((pattern) => matchesPathPattern(pathname, pattern)); /** The landing page without a v1 session is v2's marketing homepage. */ const isV2Homepage = (url: URL, request: Request): boolean => @@ -128,10 +141,19 @@ const isV2Homepage = (url: URL, request: Request): boolean => * with v1. */ const V2_MARKETING_COOKIES: ReadonlyArray = ["executor_visitor"]; -/** v2's browser telemetry proxies (product analytics and error reporting) - * live at `/api/<16 hex>` roots, which v1's API never uses. The configured - * roots (`V2_TELEMETRY_PATHS`) say which ones are v2's. */ -const TELEMETRY_ROOT_PATTERN = /^\/api\/[a-f0-9]{16}$/; +/** v2's browser telemetry lives under `/api/<16 lowercase hex>`, which v1's + * API never uses. Two settings say which paths are v2's. Both are outputs + * of v2's production infrastructure (stage `v2`; random, retained values), + * which v2 pins in its edge contract; v2 refuses to deploy stage `v2` if + * its outputs differ: + * - `V2_ANALYTICS_PROXY_PATH`, the PostHog browser proxy root (output + * `proxyPath` of v2's `executor-next-posthog` stack), forwarded with + * everything below it; + * - `V2_ERROR_TUNNEL_PATH`, the Sentry browser error tunnel (output + * `browserTunnel` of v2's `executor-next-sentry` stack), forwarded + * exactly. */ +const ANALYTICS_PROXY_PATH_PATTERN = /^\/api\/[a-f0-9]{16}$/; +const ERROR_TUNNEL_PATH_PATTERN = /^\/api\/[a-f0-9]{16}\/submit$/; const TELEMETRY_PATH_SHAPE = /^\/api\/[a-f0-9]{16}(?:\/|$)/; /** Request headers v1 never passes to v2 as sent. The cookie header carries @@ -188,8 +210,10 @@ export interface V2EdgeEnv { readonly V2_SIGN_UP_URL?: string; /** The prefix v2 puts on every connected-account OAuth `state`. */ readonly V2_OAUTH_STATE_PREFIX?: string; - /** Comma-separated `/api/<16 hex>` roots of v2's browser telemetry proxies. */ - readonly V2_TELEMETRY_PATHS?: string; + /** Root of v2's PostHog browser proxy, `/api/<16 hex>`. */ + readonly V2_ANALYTICS_PROXY_PATH?: string; + /** v2's Sentry browser error tunnel, `/api/<16 hex>/submit`. */ + readonly V2_ERROR_TUNNEL_PATH?: string; } /** What the edge needs to hand requests to v2. */ @@ -200,8 +224,10 @@ export interface V2Edge { readonly signUpUrl: URL; /** v2's connected-account OAuth state prefix (the `V2_OAUTH_STATE_PREFIX` var). */ readonly oauthStatePrefix: string; - /** Roots of v2's browser telemetry proxies (the `V2_TELEMETRY_PATHS` var). */ - readonly telemetryPaths: ReadonlyArray; + /** Root of v2's PostHog browser proxy (the `V2_ANALYTICS_PROXY_PATH` var). */ + readonly analyticsProxyPath: string; + /** v2's Sentry browser error tunnel (the `V2_ERROR_TUNNEL_PATH` var). */ + readonly errorTunnelPath: string; } /** @@ -209,18 +235,20 @@ export interface V2Edge { * when none is set (local dev, test workers), so v1 serves everything, and * the reason as a string when the deployment is broken: only some of them * set, a sign-up URL that is not absolute, a state prefix that could match a - * v1 state, or a telemetry root outside `/api/<16 hex>`. + * v1 state, or a telemetry path of the wrong shape. */ export const parseV2Edge = (env: V2EdgeEnv): V2Edge | string | null => { const service = env.V2; const signUpUrl = env.V2_SIGN_UP_URL; const oauthStatePrefix = env.V2_OAUTH_STATE_PREFIX; - const telemetryPaths = env.V2_TELEMETRY_PATHS; + const analyticsProxyPath = env.V2_ANALYTICS_PROXY_PATH; + const errorTunnelPath = env.V2_ERROR_TUNNEL_PATH; if ( service === undefined && signUpUrl === undefined && oauthStatePrefix === undefined && - telemetryPaths === undefined + analyticsProxyPath === undefined && + errorTunnelPath === undefined ) { return null; } @@ -228,9 +256,10 @@ export const parseV2Edge = (env: V2EdgeEnv): V2Edge | string | null => { service === undefined || signUpUrl === undefined || oauthStatePrefix === undefined || - telemetryPaths === undefined + analyticsProxyPath === undefined || + errorTunnelPath === undefined ) { - return "The V2 binding, V2_SIGN_UP_URL, V2_OAUTH_STATE_PREFIX and V2_TELEMETRY_PATHS must be set together"; + return "The V2 binding, V2_SIGN_UP_URL, V2_OAUTH_STATE_PREFIX, V2_ANALYTICS_PROXY_PATH and V2_ERROR_TUNNEL_PATH must be set together"; } const parsed = URL.parse(signUpUrl); if (parsed === null || (parsed.protocol !== "https:" && parsed.protocol !== "http:")) { @@ -239,11 +268,13 @@ export const parseV2Edge = (env: V2EdgeEnv): V2Edge | string | null => { if (!OAUTH_STATE_PREFIX_PATTERN.test(oauthStatePrefix)) { return "V2_OAUTH_STATE_PREFIX must be URL-safe and contain '.' or '~'"; } - const roots = telemetryPaths.split(",").map((path) => path.trim()); - if (!roots.every((root) => TELEMETRY_ROOT_PATTERN.test(root))) { - return "V2_TELEMETRY_PATHS must list /api/<16 hex> paths separated by commas"; + if (!ANALYTICS_PROXY_PATH_PATTERN.test(analyticsProxyPath)) { + return "V2_ANALYTICS_PROXY_PATH must be /api/<16 lowercase hex>"; + } + if (!ERROR_TUNNEL_PATH_PATTERN.test(errorTunnelPath)) { + return "V2_ERROR_TUNNEL_PATH must be /api/<16 lowercase hex>/submit"; } - return { service, signUpUrl: parsed, oauthStatePrefix, telemetryPaths: roots }; + return { service, signUpUrl: parsed, oauthStatePrefix, analyticsProxyPath, errorTunnelPath }; }; /** Whether a connected-account callback carries v2's state prefix. Reads the @@ -251,17 +282,20 @@ export const parseV2Edge = (env: V2EdgeEnv): V2Edge | string | null => { const isV2OAuthCallback = (url: URL, prefix: string): boolean => url.searchParams.get("state")?.startsWith(prefix) === true; -/** Whether a pathname is under one of v2's telemetry proxy roots. */ -const isV2TelemetryPath = (pathname: string, roots: ReadonlyArray): boolean => - roots.some((root) => pathname === root || pathname.startsWith(`${root}/`)); +/** Whether a pathname is v2's analytics proxy root or below it, or exactly + * v2's error tunnel. */ +const isV2TelemetryPath = (pathname: string, edge: V2Edge): boolean => + matchesPathPattern(pathname, edge.analyticsProxyPath) || + matchesPathPattern(pathname, `${edge.analyticsProxyPath}/*`) || + pathname === edge.errorTunnelPath; /** * Answer a production request that belongs to v2 and return `null` when v1 * owns it: * - sign-up (`GET`, any query) redirects to v2's sign-up page; * - a {@link isV2Path} request, a connected-account callback whose `state` - * starts with v2's prefix, or a request under one of v2's telemetry roots - * is forwarded to v2's Worker without cookies; + * starts with v2's prefix, or a request for v2's analytics proxy or error + * tunnel is forwarded to v2's Worker without cookies; * - a {@link isV2MarketingPath} request, or `/` without a v1 session, is * forwarded with only v2's marketing visitor cookie. * v2's response comes back unchanged (status, headers and body stream). @@ -290,6 +324,6 @@ export const v2EdgeResponse = (request: Request, env: V2EdgeEnv): Promise` roots of v2's browser telemetry - * proxies, which v2's marketing pages call on executor.sh. */ - V2_TELEMETRY_PATHS?: string; + /** Root of v2's PostHog browser proxy (`/api/<16 hex>`), which v2's + * marketing pages call on executor.sh; forwarded with its subpaths. */ + V2_ANALYTICS_PROXY_PATH?: string; + /** v2's Sentry browser error tunnel (`/api/<16 hex>/submit`), forwarded + * exactly. */ + V2_ERROR_TUNNEL_PATH?: string; // Shared with frontend VITE_PUBLIC_SITE_URL?: string; diff --git a/apps/cloud/src/server.ts b/apps/cloud/src/server.ts index 6faff79d3b..6e3cad473a 100644 --- a/apps/cloud/src/server.ts +++ b/apps/cloud/src/server.ts @@ -320,7 +320,8 @@ const cloudflareHandler = { // On `executor.sh`, v2 answers marketing (including `/docs` and its // telemetry proxies), sign-up, the fixed list of v2 paths and v2's - // connected-account callbacks; v1's terms stay on v1's marketing worker. + // connected-account callbacks; v1's legal pages stay on v1's marketing + // worker. const v2 = v2EdgeResponse(request, env); if (v2) return v2; diff --git a/apps/cloud/wrangler.jsonc b/apps/cloud/wrangler.jsonc index 12119057fe..baf10baffa 100644 --- a/apps/cloud/wrangler.jsonc +++ b/apps/cloud/wrangler.jsonc @@ -92,8 +92,8 @@ ], "services": [ // v1's marketing worker. On executor.sh it now serves only v1's terms of - // service and their assets (src/edge/marketing.ts); v2 serves the rest - // of marketing. + // service, privacy policy and Google OAuth disclosure, and their assets + // (src/edge/marketing.ts); v2 serves the rest of marketing. { "binding": "MARKETING", "service": "executor-marketing", @@ -152,10 +152,19 @@ // /api/oauth/callback requests carrying it go to v2. It must equal the // prefix v2 mints. "V2_OAUTH_STATE_PREFIX": "x2.", - // Roots of v2's browser telemetry proxies (product analytics, then error - // reporting) that v2's marketing pages call on executor.sh. They must - // equal the proxy paths v2's build uses. - "V2_TELEMETRY_PATHS": "/api/00e2e1f082a6ef17,/api/fd6fab1fbb4883e1", + // v2's browser telemetry on executor.sh. Both values are outputs of v2's + // production infrastructure (stage `v2`; random and retained, so they do + // not change between deploys). v2 pins them in its edge contract and + // refuses to deploy stage `v2` if its outputs differ; change them here + // only together with that contract. + // PostHog browser proxy root: output `proxyPath` of v2's + // `executor-next-posthog` stack (apps/hosted/cloud/alchemy.posthog.ts). + // Forwarded with everything below it. + "V2_ANALYTICS_PROXY_PATH": "/api/00e2e1f082a6ef17", + // Sentry browser error tunnel: output `browserTunnel` of v2's + // `executor-next-sentry` stack (apps/hosted/cloud/alchemy.sentry.ts). + // Forwarded exactly, not its root or other subpaths. + "V2_ERROR_TUNNEL_PATH": "/api/fd6fab1fbb4883e1/submit", // Keeps the /__sentry-otel-verify probe live in production: Sentry // delivery failed silently for weeks (zero events after ~Jul 30 with an // active DSN), and without this there is no way to test the pipeline diff --git a/apps/marketing/astro.config.mjs b/apps/marketing/astro.config.mjs index 4537b81615..9d3f13f420 100644 --- a/apps/marketing/astro.config.mjs +++ b/apps/marketing/astro.config.mjs @@ -30,7 +30,7 @@ export default defineConfig({ site: "https://executor.sh", output: "server", integrations: [react()], - // On executor.sh this worker now serves only v1's terms of service; v2's + // On executor.sh this worker now serves only v1's legal pages; v2's // marketing site owns `/_astro`. Build assets under their own root, which // the cloud edge (apps/cloud/src/edge/marketing.ts) forwards here. build: { assets: "_v1-marketing" }, diff --git a/apps/marketing/src/middleware.ts b/apps/marketing/src/middleware.ts index b676eba7af..a673f0b165 100644 --- a/apps/marketing/src/middleware.ts +++ b/apps/marketing/src/middleware.ts @@ -7,8 +7,8 @@ import type { MiddlewareHandler } from "astro"; // // The path MUST sit under a prefix that the cloud worker's edge forwards to // this worker. On the production apex (executor.sh) the cloud worker owns the -// custom domain and only proxies `/terms` and the `/_v1-marketing` asset root -// to executor-marketing, so we ride the asset root. +// custom domain and only proxies v1's legal pages and the `/_v1-marketing` +// asset root to executor-marketing, so we ride the asset root. const POSTHOG_INGEST_HOST = "us.i.posthog.com"; const POSTHOG_ASSETS_HOST = "us-assets.i.posthog.com"; const POSTHOG_PROXY_PATH = "/_v1-marketing/_ph"; From 2f3ac9a961963a5b2004a9e61a567d2fcbdeb2df Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:50:34 -0700 Subject: [PATCH 4/5] Forward only below v2's analytics proxy root --- apps/cloud/src/edge/marketing.ts | 12 +++++------- apps/cloud/wrangler.jsonc | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index e7a464de44..dbb4d27a29 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -147,8 +147,8 @@ const V2_MARKETING_COOKIES: ReadonlyArray = ["executor_visitor"]; * which v2 pins in its edge contract; v2 refuses to deploy stage `v2` if * its outputs differ: * - `V2_ANALYTICS_PROXY_PATH`, the PostHog browser proxy root (output - * `proxyPath` of v2's `executor-next-posthog` stack), forwarded with - * everything below it; + * `proxyPath` of v2's `executor-next-posthog` stack), forwarded below + * it (`/*`), not the root itself, which v2 does not serve; * - `V2_ERROR_TUNNEL_PATH`, the Sentry browser error tunnel (output * `browserTunnel` of v2's `executor-next-sentry` stack), forwarded * exactly. */ @@ -282,12 +282,10 @@ export const parseV2Edge = (env: V2EdgeEnv): V2Edge | string | null => { const isV2OAuthCallback = (url: URL, prefix: string): boolean => url.searchParams.get("state")?.startsWith(prefix) === true; -/** Whether a pathname is v2's analytics proxy root or below it, or exactly - * v2's error tunnel. */ +/** Whether a pathname is below v2's analytics proxy root, or exactly v2's + * error tunnel. */ const isV2TelemetryPath = (pathname: string, edge: V2Edge): boolean => - matchesPathPattern(pathname, edge.analyticsProxyPath) || - matchesPathPattern(pathname, `${edge.analyticsProxyPath}/*`) || - pathname === edge.errorTunnelPath; + matchesPathPattern(pathname, `${edge.analyticsProxyPath}/*`) || pathname === edge.errorTunnelPath; /** * Answer a production request that belongs to v2 and return `null` when v1 diff --git a/apps/cloud/wrangler.jsonc b/apps/cloud/wrangler.jsonc index baf10baffa..3fc0f540a1 100644 --- a/apps/cloud/wrangler.jsonc +++ b/apps/cloud/wrangler.jsonc @@ -159,7 +159,7 @@ // only together with that contract. // PostHog browser proxy root: output `proxyPath` of v2's // `executor-next-posthog` stack (apps/hosted/cloud/alchemy.posthog.ts). - // Forwarded with everything below it. + // Forwarded below it (`/*`), not the root itself. "V2_ANALYTICS_PROXY_PATH": "/api/00e2e1f082a6ef17", // Sentry browser error tunnel: output `browserTunnel` of v2's // `executor-next-sentry` stack (apps/hosted/cloud/alchemy.sentry.ts). From ffbf4969547259c0f2b13b337840a54efd719f67 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:23:25 -0700 Subject: [PATCH 5/5] Redirect slashed v2 pages on executor.sh to the page --- apps/cloud/src/edge/marketing.ts | 34 +++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index dbb4d27a29..9f19e58128 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -131,6 +131,27 @@ const V2_MARKETING_PATHS: ReadonlyArray = [ export const isV2MarketingPath = (pathname: string): boolean => V2_MARKETING_PATHS.some((pattern) => matchesPathPattern(pathname, pattern)); +/** v2's pages that match only themselves (`/pricing`, `/home`, + * `/about-executor`, `/google-workspace`): exact patterns that are not + * files and have no `/x/*` beside them. Their slashed form (`/pricing/`) + * matches no pattern, so v1's sign-in gate would answer it. */ +const V2_EXACT_PAGES: ReadonlySet = new Set( + V2_MARKETING_PATHS.filter( + (pattern) => + !pattern.endsWith("/*") && + !pattern.includes(".") && + !V2_MARKETING_PATHS.includes(`${pattern}/*`), + ), +); + +/** The page a slashed v2 page (`/pricing/`) canonicalizes to (`/pricing`), + * or `null`. Deeper paths (`/pricing/extra`) stay with v1. */ +export const v2PageForSlashedPath = (pathname: string): string | null => { + if (!pathname.endsWith("/")) return null; + const page = pathname.slice(0, -1); + return V2_EXACT_PAGES.has(page) ? page : null; +}; + /** The landing page without a v1 session is v2's marketing homepage. */ const isV2Homepage = (url: URL, request: Request): boolean => url.pathname === "/" && parseCookie(request.headers.get("cookie"), SESSION_COOKIE) === null; @@ -291,6 +312,8 @@ const isV2TelemetryPath = (pathname: string, edge: V2Edge): boolean => * Answer a production request that belongs to v2 and return `null` when v1 * owns it: * - sign-up (`GET`, any query) redirects to v2's sign-up page; + * - a `GET` or `HEAD` for a slashed v2 page (`/pricing/`) redirects (308) + * to the page, query kept, as v2's site does on its own hosts; * - a {@link isV2Path} request, a connected-account callback whose `state` * starts with v2's prefix, or a request for v2's analytics proxy or error * tunnel is forwarded to v2's Worker without cookies; @@ -307,8 +330,12 @@ export const v2EdgeResponse = (request: Request, env: V2EdgeEnv): Promise