diff --git a/router/README.md b/router/README.md index 37f9454d..028cfa4e 100644 --- a/router/README.md +++ b/router/README.md @@ -1,5 +1,6 @@ Route precedence: +- `/meet-with-will`, `/meet-with-khaliq`, and `/virtual-office` redirect to their external meeting destinations at the edge. - `/cloud*` routes to the Agent Relay Cloud Next.js app. - `/observer/file*` routes to the RelayFile observer app, expected to be built with `basePath: "/observer/file"`. - `/observer*` routes to the Relaycast observer app. diff --git a/router/index.ts b/router/index.ts index e6e85fda..e8b63ddd 100644 --- a/router/index.ts +++ b/router/index.ts @@ -31,6 +31,11 @@ const FILE_OBSERVER_PATH_PREFIX = "/observer/file"; const OBSERVER_PATH_PREFIX = "/observer"; const CLOUD_PATH_PREFIX = "/cloud"; const WEBHOOK_ORIGIN_FLAG_KEY = "WEBHOOK_ORIGIN"; +const VANITY_REDIRECTS = new Map([ + ["/meet-with-will", "https://calendar.app.google/RqLuQyT3dYe5e2YdA"], + ["/meet-with-khaliq", "https://calendly.com/khaliq-agent-relay/30min"], + ["/virtual-office", "https://meet.google.com/ijx-gpfb-brt"], +]); // Header set by webhook-worker's queue consumer // (`packages/webhook-worker/src/queue-consumer.ts`'s `buildForwardHeaders`) on @@ -77,6 +82,17 @@ function isCloudPath(pathname: string): boolean { return isPathWithinPrefix(pathname, CLOUD_PATH_PREFIX); } +export function getVanityRedirect(hostname: string, pathname: string): string | undefined { + if (hostname !== PRIMARY_HOST) { + return undefined; + } + + const normalizedPathname = (pathname.length > 1 + ? pathname.replace(/\/$/, "") + : pathname).toLowerCase(); + return VANITY_REDIRECTS.get(normalizedPathname); +} + // True only for the exact paths the webhook worker knows how to handle. Used // to gate worker forwarding so unrelated routes under /api/v1/webhooks/* (e.g. // the Composio OAuth callback) still reach the Lambda. @@ -309,6 +325,15 @@ export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); + const vanityRedirect = getVanityRedirect(url.hostname, url.pathname); + if (vanityRedirect) { + const redirectUrl = new URL(vanityRedirect); + url.searchParams.forEach((value, key) => { + redirectUrl.searchParams.set(key, value); + }); + return Response.redirect(redirectUrl.toString(), 302); + } + // Per-key rate limiting runs BEFORE any worker routing so a runaway // workspace gets bounded everywhere — including webhook ingress and // /cloud* traffic. The bypass list inside maybeRateLimit exempts diff --git a/router/test/vanity-redirects.test.ts b/router/test/vanity-redirects.test.ts new file mode 100644 index 00000000..7578c4b4 --- /dev/null +++ b/router/test/vanity-redirects.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from "vitest"; + +import worker, { getVanityRedirect } from "../index.js"; + +const redirects = [ + ["/meet-with-will", "https://calendar.app.google/RqLuQyT3dYe5e2YdA"], + ["/meet-with-khaliq", "https://calendly.com/khaliq-agent-relay/30min"], + ["/virtual-office", "https://meet.google.com/ijx-gpfb-brt"], +] as const; + +function buildCtx(): ExecutionContext { + return { + waitUntil: () => undefined, + passThroughOnException: () => undefined, + } as unknown as ExecutionContext; +} + +describe("router vanity redirects", () => { + it.each(redirects)("redirects %s at the edge", async (path, destination) => { + const cloudWebWorker = { fetch: vi.fn() }; + const response = await worker.fetch( + new Request(`https://agentrelay.com${path}?utm_source=test`), + { + CLOUD_APP_ORIGIN: "https://origin.test.invalid", + CLOUD_WEB_WORKER: cloudWebWorker, + }, + buildCtx(), + ); + + expect(response.status).toBe(302); + expect(response.headers.get("location")).toBe(destination); + expect(cloudWebWorker.fetch).not.toHaveBeenCalled(); + }); + + it.each(redirects)("accepts a trailing slash for %s", (path, destination) => { + expect(getVanityRedirect("agentrelay.com", `${path}/`)).toBe(destination); + }); + + it("does not redirect another hostname or a nested path", () => { + expect(getVanityRedirect("origin-web.agentrelay.com", "/meet-with-will")).toBeUndefined(); + expect(getVanityRedirect("agentrelay.com", "/meet-with-will/details")).toBeUndefined(); + }); +});