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
141 changes: 135 additions & 6 deletions apps/cloud/src/edge/marketing.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// ---------------------------------------------------------------------------
// Marketing routes — proxied to the marketing worker via service binding.
// The `executor.sh` edge — decides which Worker answers a public request.
//
// On the production domain (`executor.sh`), marketing paths and the
// unauthenticated landing page are served by the separate `executor-marketing`
// worker. This module deliberately has no TanStack Start or cloud application
// imports: the Worker entry calls it before loading the Start server graph.
// On the production domain (`executor.sh`):
// - marketing paths and the unauthenticated landing page go to the separate
// `executor-marketing` worker;
// - 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.
// 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";
Expand Down Expand Up @@ -32,6 +38,8 @@ const MARKETING_PATHS = [

const SESSION_COOKIE = "wos-session";

const PRODUCTION_HOST = "executor.sh";

/** Whether an exact pathname belongs to the public marketing worker. */
export const isMarketingPath = (pathname: string): boolean =>
MARKETING_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`));
Expand All @@ -42,7 +50,7 @@ export const isMarketingPath = (pathname: string): boolean =>
*/
export const marketingProxyRequest = (request: Request): Request | null => {
const url = new URL(request.url);
if (url.hostname !== "executor.sh") return null;
if (url.hostname !== PRODUCTION_HOST) return null;

const shouldProxy =
isMarketingPath(url.pathname) ||
Expand All @@ -52,3 +60,124 @@ export const marketingProxyRequest = (request: Request): Request | null => {
if (url.pathname === "/home") url.pathname = "/";
return new Request(url, request);
};

// ---------------------------------------------------------------------------
// v2 on `executor.sh`
// ---------------------------------------------------------------------------

const SIGN_UP_PATHS: ReadonlySet<string> = new Set(["/sign-up", "/signup"]);

/** A path pattern is an exact path, or `/x/*`, which matches every path
* that starts with `/x/` (and not `/x` itself). */
const matchesPathPattern = (pathname: string, pattern: string): boolean =>
pattern.endsWith("/*") ? pathname.startsWith(pattern.slice(0, -1)) : pathname === pattern;

/** Path patterns v2 answers on `executor.sh`. `/git/*` never matches
* `/gitlab/...`. v2's outbound OAuth client metadata document stays off this
* list: its move to `executor.sh` is pending, and v1's own document
* (`/oauth/client-id-metadata.json`) stays with v1. */
const V2_PATHS: ReadonlyArray<string> = [
// Sign-in issuer metadata for the issuer `https://executor.sh/api/auth`.
"/.well-known/oauth-authorization-server/api/auth",
"/git/*",
"/.well-known/agent-skills/*",
];

/** v2's social sign-in callback is `/api/auth/callback/<provider>`. v1's
* WorkOS callback is the bare `/api/auth/callback`, which stays with v1. */
const SOCIAL_CALLBACK_PREFIX = "/api/auth/callback/";

const isSocialCallbackPath = (pathname: string): boolean => {
if (!pathname.startsWith(SOCIAL_CALLBACK_PREFIX)) return false;
const provider = pathname.slice(SOCIAL_CALLBACK_PREFIX.length);
return provider.length > 0 && !provider.includes("/");
};

/** Whether a pathname is a sign-up entry point that redirects to v2. */
export const isSignUpPath = (pathname: string): boolean => SIGN_UP_PATHS.has(pathname);

/** Whether `executor.sh` forwards a pathname to v2's Worker. */
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. */
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.
*/
export const v2ForwardRequest = (request: Request): Request => {
const headers = new Headers(request.headers);
for (const name of V2_STRIPPED_HEADERS) headers.delete(name);
return new Request(request, { headers, redirect: "manual" });
};

/** The Worker that serves v2, reached over a service binding. */
export interface V2Service {
readonly fetch: (request: Request) => Promise<Response>;
}

/** What the edge needs to hand requests to v2. */
export interface V2Edge {
/** v2's Worker. */
readonly service: V2Service;
/** Absolute URL of v2's sign-up page (the `V2_SIGN_UP_URL` var). */
readonly signUpUrl: URL;
}

/**
* Parse the edge's v2 settings from the Worker environment. Returns `null`
* when neither is set (local dev, test workers), so v1 serves everything, and
* the reason as a string when the deployment is broken: only one of them set,
* or a sign-up URL that is not absolute.
*/
export const parseV2Edge = (
service: V2Service | undefined,
signUpUrl: string | undefined,
): V2Edge | string | null => {
if (service === undefined && signUpUrl === undefined) return null;
if (service === undefined || signUpUrl === undefined) {
return "The V2 binding and V2_SIGN_UP_URL must be set together";
}
const parsed = URL.parse(signUpUrl);
if (parsed === null || (parsed.protocol !== "https:" && parsed.protocol !== "http:")) {
return "V2_SIGN_UP_URL must be an absolute http(s) URL";
}
return { service, signUpUrl: parsed };
};

/**
* 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 to
* v2's Worker and return its response unchanged (status, headers and body
* stream). Returns `null` when v1 owns the request. Broken settings answer
* the requests the edge owns with a 500 and leave the rest of v1 serving.
*/
export const v2EdgeResponse = (
request: Request,
service: V2Service | undefined,
signUpUrl: string | undefined,
): Promise<Response> | null => {
const url = new URL(request.url);
if (url.hostname !== PRODUCTION_HOST) return null;

const signUp = isSignUpPath(url.pathname) && request.method === "GET";
if (!signUp && !isV2Path(url.pathname)) return null;
const edge = parseV2Edge(service, signUpUrl);
if (edge === null) return null;
if (typeof edge === "string") {
console.error(`executor.sh v2 edge misconfigured: ${edge}`);
return Promise.resolve(new Response("Service misconfigured", { status: 500 }));
}
if (signUp) return Promise.resolve(Response.redirect(edge.signUpUrl.href, 302));
return edge.service.fetch(v2ForwardRequest(request));
};
7 changes: 7 additions & 0 deletions apps/cloud/src/env-augment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ declare global {
MCP_RESIDENT_RUNTIME_SOFT_CAP?: string;
NODE_ENV?: string;

// v2 on executor.sh (wrangler.jsonc `services` + `vars`). Optional so
// local dev and test workers without them serve everything from v1.
/** Service binding to v2's API Worker; `edge/marketing.ts` forwards to it. */
V2?: Fetcher;
/** Absolute URL `/sign-up` and `/signup` redirect to. */
V2_SIGN_UP_URL?: string;

// Shared with frontend
VITE_PUBLIC_SITE_URL?: string;
VITE_PUBLIC_OTLP_TRACES_URL?: string;
Expand Down
6 changes: 5 additions & 1 deletion apps/cloud/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as Sentry from "@sentry/cloudflare";
import handler from "@tanstack/react-start/server-entry";

import { isAppOwnedPath, servedByAppPlane } from "./app-paths";
import { marketingProxyRequest } from "./edge/marketing";
import { marketingProxyRequest, v2EdgeResponse } from "./edge/marketing";
import { passthroughResponse } from "./edge/passthrough";
import { withPrivateReferrerPolicy } from "./edge/referrer-policy";
import { runWorkOsEventsSync } from "./auth/workos-events-runner";
Expand Down Expand Up @@ -318,6 +318,10 @@ const cloudflareHandler = {
prewarmAppPlane(ctx);
}

// Sign-up and the fixed list of v2 paths on `executor.sh` go to v2.
const v2 = v2EdgeResponse(request, env.V2, env.V2_SIGN_UP_URL);
if (v2) return v2;

const marketingRequest = marketingProxyRequest(request);
const marketing: Fetcher | undefined = env.MARKETING;
if (marketingRequest && marketing) return marketing.fetch(marketingRequest);
Expand Down
9 changes: 9 additions & 0 deletions apps/cloud/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@
"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`.
{
"binding": "V2",
"service": "executor-next-hosted-api-v2-qs32brgjwvt7ytx4",
},
],
"hyperdrive": [
{
Expand Down Expand Up @@ -133,6 +140,8 @@
},
"vars": {
"VITE_PUBLIC_SITE_URL": "https://executor.sh",
// Where /sign-up and /signup redirect: v2's sign-up page.
"V2_SIGN_UP_URL": "https://v2.executor.sh/login?mode=signup",
// 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
Expand Down
9 changes: 9 additions & 0 deletions packages/core/api/src/account/org-slug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const ORG_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){1,47}$/;
* - Marketing worker: home, setup, privacy, terms, blog, pricing, careers,
* changelog, _astro (executor.sh edge routes; the
* non-route names are cheap insurance)
* - v2 on executor.sh: git (Git smart HTTP remotes the executor.sh edge
* forwards to v2), apps and experiments (v2 marketing
* pages; no v1 organization uses them), plus oauth,
* api, app, mcp, docs, sign-up, signup, pricing and
* blog listed elsewhere here
* - Infra: assets (vite build output), cdn-cgi (Cloudflare),
* static, public, favicon.ico, robots.txt, sitemap.xml
* - Auth flows: auth, oauth, callback, logout, signin, signout,
Expand Down Expand Up @@ -69,6 +74,10 @@ export const RESERVED_ORG_SLUGS: ReadonlySet<string> = new Set([
"careers",
"changelog",
"_astro",
// v2 on executor.sh
"git",
"apps",
"experiments",
// infra
"assets",
"cdn-cgi",
Expand Down
Loading