Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 198 additions & 59 deletions apps/cloud/src/edge/marketing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,57 @@
// 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, 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.
// ---------------------------------------------------------------------------

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 legal pages on the `executor-marketing` worker
// ---------------------------------------------------------------------------

/** Whether an exact pathname belongs to the public marketing worker. */
/** 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<string> = [
"/terms",
"/privacy",
"/google-oauth",
"/_v1-marketing",
];

/** 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 legal pages (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);
};

Expand Down Expand Up @@ -103,23 +96,116 @@ export const isV2Path = (pathname: string): boolean =>
V2_PATHS.some((pattern) => matchesPathPattern(pathname, pattern)) ||
isSocialCallbackPath(pathname);

/** 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. */
/** 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<string> = [
"/home",
"/pricing",
"/pricing.md",
"/index.md",
"/setup-prompt.md",
"/llms.txt",
"/about-executor",
"/google-workspace",
"/blog",
"/blog/*",
"/docs",
"/docs/*",
"/apps",
"/apps/*",
"/experiments/*",
"/_astro/*",
"/authors/*",
"/og-image.png",
"/pattern-graph-paper.svg",
];

/** 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((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<string> = 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;

/** 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<string> = ["executor_visitor"];

/** 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 below
* it (`<root>/*`), 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. */
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
* 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;

/**
* Project an `executor.sh` request onto the request sent to v2's Worker.
*
* 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
* followed: v2 answers callbacks with a redirect to its own host.
* 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<string> = [],
): 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" });
};

Expand All @@ -145,6 +231,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;
/** 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. */
Expand All @@ -155,24 +245,42 @@ export interface V2Edge {
readonly signUpUrl: URL;
/** v2's connected-account OAuth state prefix (the `V2_OAUTH_STATE_PREFIX` var). */
readonly oauthStatePrefix: string;
/** 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;
}

/**
* 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 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;
if (service === undefined && signUpUrl === undefined && oauthStatePrefix === undefined) {
const analyticsProxyPath = env.V2_ANALYTICS_PROXY_PATH;
const errorTunnelPath = env.V2_ERROR_TUNNEL_PATH;
if (
service === undefined &&
signUpUrl === undefined &&
oauthStatePrefix === undefined &&
analyticsProxyPath === undefined &&
errorTunnelPath === 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 ||
analyticsProxyPath === undefined ||
errorTunnelPath === undefined
) {
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:")) {
Expand All @@ -181,40 +289,71 @@ 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 };
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, analyticsProxyPath, errorTunnelPath };
};

/** Whether a connected-account callback carries v2's state prefix. Reads the
* query only: a callback without `state` in its query stays with v1. */
const isV2OAuthCallback = (url: URL, prefix: string): boolean =>
url.searchParams.get("state")?.startsWith(prefix) === true;

/** 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}/*`) || pathname === edge.errorTunnelPath;

/**
* 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 `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;
* - 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<Response> | null => {
const url = new URL(request.url);
if (url.hostname !== PRODUCTION_HOST) return null;

const signUp = isSignUpPath(url.pathname) && request.method === "GET";
const slashedPage =
request.method === "GET" || request.method === "HEAD"
? v2PageForSlashedPath(url.pathname)
: null;
const marketing = isV2MarketingPath(url.pathname) || isV2Homepage(url, request);
const owned = signUp || slashedPage !== null || 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 (slashedPage !== null) {
const page = new URL(url);
page.pathname = slashedPage;
return Promise.resolve(Response.redirect(page.href, 308));
}
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)) return null;
return edge.service.fetch(v2ForwardRequest(request));
};
6 changes: 6 additions & 0 deletions apps/cloud/src/env-augment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ 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;
/** 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;
Expand Down
8 changes: 6 additions & 2 deletions apps/cloud/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,10 @@ 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 legal pages stay on v1's marketing
// worker.
const v2 = v2EdgeResponse(request, env);
if (v2) return v2;

Expand All @@ -333,6 +335,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;
Expand Down
Loading
Loading