From 790c3afc2f644f892062642151554e99d9c78af0 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:24:37 -0700 Subject: [PATCH 1/4] Route sign-up and v2 paths on executor.sh to v2 --- apps/cloud/src/edge/marketing.ts | 127 +++++++++++++++++++++- apps/cloud/src/env-augment.d.ts | 7 ++ apps/cloud/src/server.ts | 7 +- apps/cloud/wrangler.jsonc | 9 ++ packages/core/api/src/account/org-slug.ts | 5 + 5 files changed, 148 insertions(+), 7 deletions(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index 3e0fa5493d..0af2e9b551 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -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 exact v2 paths (sign-in issuer metadata, social sign-in +// callbacks, the OAuth client metadata document, 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"; @@ -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}/`)); @@ -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) || @@ -52,3 +60,110 @@ 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 = new Set(["/sign-up", "/signup"]); + +/** Paths v2 answers on `executor.sh`, matched exactly. */ +const V2_EXACT_PATHS: ReadonlySet = new Set([ + // Sign-in issuer metadata for the issuer `https://executor.sh/api/auth`. + "/.well-known/oauth-authorization-server/api/auth", + "/api/auth/.well-known/openid-configuration", + // Outbound OAuth client metadata document. v1's own document lives at + // `/oauth/client-id-metadata.json`, which stays with v1. + "/oauth/client-metadata.json", +]); + +/** Path trees v2 answers on `executor.sh`. Each prefix ends in `/`, so + * `/gitlab/...` never matches `/git/`. */ +const V2_PATH_PREFIXES: ReadonlyArray = ["/git/", "/.well-known/agent-skills/"]; + +/** v2's social sign-in callback is `/api/auth/callback/`. 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_EXACT_PATHS.has(pathname) || + V2_PATH_PREFIXES.some((prefix) => pathname.startsWith(prefix)) || + 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; +} + +/** 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 the binding or the sign-up URL is absent or the URL is not absolute, + * so hosts without them (local dev, test workers) keep serving everything + * from v1. + */ +export const parseV2Edge = ( + service: V2Service | undefined, + signUpUrl: string | undefined, +): V2Edge | null => { + if (service === undefined || signUpUrl === undefined) return null; + const parsed = URL.parse(signUpUrl); + if (parsed === null || (parsed.protocol !== "https:" && parsed.protocol !== "http:")) { + return null; + } + 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. + */ +export const v2EdgeResponse = (request: Request, edge: V2Edge): Promise | null => { + const url = new URL(request.url); + if (url.hostname !== PRODUCTION_HOST) return null; + + if (isSignUpPath(url.pathname) && request.method === "GET") { + return Promise.resolve(Response.redirect(edge.signUpUrl.href, 302)); + } + if (isV2Path(url.pathname)) return edge.service.fetch(v2ForwardRequest(request)); + return null; +}; diff --git a/apps/cloud/src/env-augment.d.ts b/apps/cloud/src/env-augment.d.ts index 017570cf1a..4af210008e 100644 --- a/apps/cloud/src/env-augment.d.ts +++ b/apps/cloud/src/env-augment.d.ts @@ -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; diff --git a/apps/cloud/src/server.ts b/apps/cloud/src/server.ts index 2dde0e8403..fa41d3582d 100644 --- a/apps/cloud/src/server.ts +++ b/apps/cloud/src/server.ts @@ -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, parseV2Edge, v2EdgeResponse } from "./edge/marketing"; import { passthroughResponse } from "./edge/passthrough"; import { withPrivateReferrerPolicy } from "./edge/referrer-policy"; import { runWorkOsEventsSync } from "./auth/workos-events-runner"; @@ -318,6 +318,11 @@ const cloudflareHandler = { prewarmAppPlane(ctx); } + // Sign-up and the fixed list of v2 paths on `executor.sh` go to v2. + const v2Edge = parseV2Edge(env.V2, env.V2_SIGN_UP_URL); + const v2 = v2Edge && v2EdgeResponse(request, v2Edge); + if (v2) return v2; + const marketingRequest = marketingProxyRequest(request); const marketing: Fetcher | undefined = env.MARKETING; if (marketingRequest && marketing) return marketing.fetch(marketingRequest); diff --git a/apps/cloud/wrangler.jsonc b/apps/cloud/wrangler.jsonc index 04da6b0e34..44be69363a 100644 --- a/apps/cloud/wrangler.jsonc +++ b/apps/cloud/wrangler.jsonc @@ -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": [ { @@ -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 diff --git a/packages/core/api/src/account/org-slug.ts b/packages/core/api/src/account/org-slug.ts index addd842831..2950130233 100644 --- a/packages/core/api/src/account/org-slug.ts +++ b/packages/core/api/src/account/org-slug.ts @@ -27,6 +27,9 @@ 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), 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, @@ -69,6 +72,8 @@ export const RESERVED_ORG_SLUGS: ReadonlySet = new Set([ "careers", "changelog", "_astro", + // v2 on executor.sh + "git", // infra "assets", "cdn-cgi", From 231698ef0ae17f1ff6e76ed6d193cbecde037baa Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:33:21 -0700 Subject: [PATCH 2/4] Fail edge requests on broken v2 settings instead of serving them from v1 --- apps/cloud/src/edge/marketing.ts | 35 +++++++++++++++++++++----------- apps/cloud/src/server.ts | 5 ++--- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index 0af2e9b551..2aa3e5b602 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -135,18 +135,22 @@ export interface V2Edge { /** * Parse the edge's v2 settings from the Worker environment. Returns `null` - * when the binding or the sign-up URL is absent or the URL is not absolute, - * so hosts without them (local dev, test workers) keep serving everything - * from v1. + * when neither the binding nor the sign-up URL is set, so hosts without them + * (local dev, test workers) keep serving everything from v1. Throws when only + * one is set or the URL is not absolute, so a broken deployment fails loudly + * instead of silently serving sign-up from v1. */ export const parseV2Edge = ( service: V2Service | undefined, signUpUrl: string | undefined, ): V2Edge | null => { - if (service === undefined || signUpUrl === undefined) return null; + if (service === undefined && signUpUrl === undefined) return null; + if (service === undefined || signUpUrl === undefined) { + throw new Error("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 null; + throw new Error("V2_SIGN_UP_URL must be an absolute http(s) URL"); } return { service, signUpUrl: parsed }; }; @@ -155,15 +159,22 @@ export const parseV2Edge = ( * 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. + * stream). Returns `null` when v1 owns the request. The settings are parsed + * only for requests the edge owns, so a broken setting fails those requests + * and leaves the rest of v1 serving. */ -export const v2EdgeResponse = (request: Request, edge: V2Edge): Promise | null => { +export const v2EdgeResponse = ( + request: Request, + service: V2Service | undefined, + signUpUrl: string | undefined, +): Promise | null => { const url = new URL(request.url); if (url.hostname !== PRODUCTION_HOST) return null; - if (isSignUpPath(url.pathname) && request.method === "GET") { - return Promise.resolve(Response.redirect(edge.signUpUrl.href, 302)); - } - if (isV2Path(url.pathname)) return edge.service.fetch(v2ForwardRequest(request)); - 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 (signUp) return Promise.resolve(Response.redirect(edge.signUpUrl.href, 302)); + return edge.service.fetch(v2ForwardRequest(request)); }; diff --git a/apps/cloud/src/server.ts b/apps/cloud/src/server.ts index fa41d3582d..a9e98183fd 100644 --- a/apps/cloud/src/server.ts +++ b/apps/cloud/src/server.ts @@ -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, parseV2Edge, v2EdgeResponse } 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"; @@ -319,8 +319,7 @@ const cloudflareHandler = { } // Sign-up and the fixed list of v2 paths on `executor.sh` go to v2. - const v2Edge = parseV2Edge(env.V2, env.V2_SIGN_UP_URL); - const v2 = v2Edge && v2EdgeResponse(request, v2Edge); + const v2 = v2EdgeResponse(request, env.V2, env.V2_SIGN_UP_URL); if (v2) return v2; const marketingRequest = marketingProxyRequest(request); From 6eea400e1db73afdbe625a00a21a67376d76b410 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:35:10 -0700 Subject: [PATCH 3/4] Answer edge requests with a 500 on broken v2 settings --- apps/cloud/src/edge/marketing.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index 2aa3e5b602..30e8b8da3f 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -135,22 +135,21 @@ export interface V2Edge { /** * Parse the edge's v2 settings from the Worker environment. Returns `null` - * when neither the binding nor the sign-up URL is set, so hosts without them - * (local dev, test workers) keep serving everything from v1. Throws when only - * one is set or the URL is not absolute, so a broken deployment fails loudly - * instead of silently serving sign-up from v1. + * 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 | null => { +): V2Edge | string | null => { if (service === undefined && signUpUrl === undefined) return null; if (service === undefined || signUpUrl === undefined) { - throw new Error("The V2 binding and V2_SIGN_UP_URL must be set together"); + 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:")) { - throw new Error("V2_SIGN_UP_URL must be an absolute http(s) URL"); + return "V2_SIGN_UP_URL must be an absolute http(s) URL"; } return { service, signUpUrl: parsed }; }; @@ -159,9 +158,8 @@ export const parseV2Edge = ( * 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. The settings are parsed - * only for requests the edge owns, so a broken setting fails those requests - * and leaves the rest of v1 serving. + * 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, @@ -175,6 +173,10 @@ export const v2EdgeResponse = ( 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)); }; From 5bb09cdbe4aae55a29a50b4e48f1a2595f79735f Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:33:31 -0700 Subject: [PATCH 4/4] Forward only v2's contract paths and reserve the apps and experiments slugs --- apps/cloud/src/edge/marketing.ts | 33 ++++++++++++----------- packages/core/api/src/account/org-slug.ts | 8 ++++-- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/apps/cloud/src/edge/marketing.ts b/apps/cloud/src/edge/marketing.ts index 30e8b8da3f..62d2b77b98 100644 --- a/apps/cloud/src/edge/marketing.ts +++ b/apps/cloud/src/edge/marketing.ts @@ -5,9 +5,9 @@ // - 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 exact v2 paths (sign-in issuer metadata, social sign-in -// callbacks, the OAuth client metadata document, Git smart HTTP and the -// agent skills index) is forwarded to v2's Worker over a service binding. +// - 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. @@ -67,19 +67,21 @@ export const marketingProxyRequest = (request: Request): Request | null => { const SIGN_UP_PATHS: ReadonlySet = new Set(["/sign-up", "/signup"]); -/** Paths v2 answers on `executor.sh`, matched exactly. */ -const V2_EXACT_PATHS: ReadonlySet = new Set([ +/** 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 = [ // Sign-in issuer metadata for the issuer `https://executor.sh/api/auth`. "/.well-known/oauth-authorization-server/api/auth", - "/api/auth/.well-known/openid-configuration", - // Outbound OAuth client metadata document. v1's own document lives at - // `/oauth/client-id-metadata.json`, which stays with v1. - "/oauth/client-metadata.json", -]); - -/** Path trees v2 answers on `executor.sh`. Each prefix ends in `/`, so - * `/gitlab/...` never matches `/git/`. */ -const V2_PATH_PREFIXES: ReadonlyArray = ["/git/", "/.well-known/agent-skills/"]; + "/git/*", + "/.well-known/agent-skills/*", +]; /** v2's social sign-in callback is `/api/auth/callback/`. v1's * WorkOS callback is the bare `/api/auth/callback`, which stays with v1. */ @@ -96,8 +98,7 @@ export const isSignUpPath = (pathname: string): boolean => SIGN_UP_PATHS.has(pat /** Whether `executor.sh` forwards a pathname to v2's Worker. */ export const isV2Path = (pathname: string): boolean => - V2_EXACT_PATHS.has(pathname) || - V2_PATH_PREFIXES.some((prefix) => pathname.startsWith(prefix)) || + V2_PATHS.some((pattern) => matchesPathPattern(pathname, pattern)) || isSocialCallbackPath(pathname); /** Request headers v1 never passes to v2. The cookie header carries v1's diff --git a/packages/core/api/src/account/org-slug.ts b/packages/core/api/src/account/org-slug.ts index 2950130233..5b83758805 100644 --- a/packages/core/api/src/account/org-slug.ts +++ b/packages/core/api/src/account/org-slug.ts @@ -28,8 +28,10 @@ const ORG_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){1,47}$/; * 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), plus oauth, api, app, mcp, docs, - * sign-up, signup, pricing and blog listed elsewhere here + * 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, @@ -74,6 +76,8 @@ export const RESERVED_ORG_SLUGS: ReadonlySet = new Set([ "_astro", // v2 on executor.sh "git", + "apps", + "experiments", // infra "assets", "cdn-cgi",