Skip to content
Merged
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
7 changes: 5 additions & 2 deletions .github/workflows/deploy-router.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,22 @@ jobs:

# Worker secrets ship atomically with the deploy. Only secrets that are
# present are written; an absent ANALYTICS_VISITOR_HASH_SECRET leaves
# guide-fetch analytics failing closed (no data points).
# guide-fetch analytics failing closed (no data points), and an absent
# OPENAI_APPS_CHALLENGE_TOKEN leaves /.well-known/openai-apps-challenge
# returning 404.
- name: Prepare optional Worker secrets
shell: bash
env:
ANALYTICS_VISITOR_HASH_SECRET: ${{ secrets.ANALYTICS_VISITOR_HASH_SECRET }}
OPENAI_APPS_CHALLENGE_TOKEN: ${{ secrets.OPENAI_APPS_CHALLENGE_TOKEN }}
run: |
set -euo pipefail
umask 077
export ROUTER_SECRETS_FILE="$RUNNER_TEMP/router-secrets.json"
node --input-type=module <<'NODE'
import { writeFileSync } from "node:fs";

const names = ["ANALYTICS_VISITOR_HASH_SECRET"];
const names = ["ANALYTICS_VISITOR_HASH_SECRET", "OPENAI_APPS_CHALLENGE_TOKEN"];
const secrets = Object.fromEntries(names.flatMap((name) => {
const value = process.env[name];
return value ? [[name, value]] : [];
Expand Down
48 changes: 48 additions & 0 deletions router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ interface Env extends AnalyticsEnv {
fetch(request: Request): Promise<Response>;
};
WEBHOOK_WORKER_ORIGIN?: string;
// Worker secret holding OpenAI's apps domain-verification token. Shipped by
// deploy-router.yml from the OPENAI_APPS_CHALLENGE_TOKEN GitHub Actions
// secret; never committed. Unset means the challenge path 404s.
OPENAI_APPS_CHALLENGE_TOKEN?: string;
}

function hasRecorderEnv(env: Env): env is Env & RecorderEnv {
Expand Down Expand Up @@ -688,10 +692,54 @@ async function fetchRelayAgent(request: Request, url: URL, env: Env): Promise<Re
return globalThis.fetch(buildRelayAgentRequest(request, url, validatedRelayAgentOrigin(origin)));
}

// OpenAI apps domain verification. OpenAI fetches this apex path and expects
// the bare token as text/plain with a 200 — no HTML, JSON or redirect. The
// token comes from the OPENAI_APPS_CHALLENGE_TOKEN Worker secret; when it is
// unset or blank the path 404s rather than falling through to the marketing
// site. Answered before any upstream dispatch so the token never reaches the
// traffic recorder.
const OPENAI_APPS_CHALLENGE_PATH = "/.well-known/openai-apps-challenge";

export function getOpenAiAppsChallengeResponse(
request: Request,
url: URL,
env: Pick<Env, "OPENAI_APPS_CHALLENGE_TOKEN">,
): Response | null {
if (url.hostname !== PRIMARY_HOST || url.pathname !== OPENAI_APPS_CHALLENGE_PATH) {
return null;
}

const headers = {
"content-type": "text/plain; charset=utf-8",
"cache-control": "no-store",
};
if (request.method !== "GET" && request.method !== "HEAD") {
return new Response("Method Not Allowed", {
status: 405,
headers: { ...headers, allow: "GET, HEAD" },
});
}

const token = env.OPENAI_APPS_CHALLENGE_TOKEN?.trim();
if (!token) {
return new Response("Not Found", { status: 404, headers });
}

return new Response(request.method === "HEAD" ? null : token, {
status: 200,
headers,
});
}

const router = {
async fetch(request: Request, env: Env, ctx: ExecutionContext, timing: EdgeTiming): Promise<Response> {
const url = new URL(request.url);

const openAiAppsChallenge = getOpenAiAppsChallengeResponse(request, url, env);
if (openAiAppsChallenge) {
return openAiAppsChallenge;
}

const oauthWellKnownPath = url.hostname === PRIMARY_HOST
&& (request.method === "GET" || request.method === "HEAD")
? rewriteCloudOAuthWellKnownPath(url.pathname)
Expand Down
98 changes: 98 additions & 0 deletions router/test/openai-apps-challenge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it, vi } from "vitest";

import worker from "../index.js";

const TOKEN = "test-challenge-token-not-real";

function buildEnv(overrides: Record<string, unknown> = {}) {
return {
CLOUD_APP_ORIGIN: "https://origin.test.invalid",
CLOUD_WEB_WORKER: { fetch: vi.fn(async () => new Response("cloud", { status: 200 })) },
...overrides,
} as unknown as Parameters<typeof worker.fetch>[1];
}

const ctx = { waitUntil: () => undefined, passThroughOnException: () => undefined } as unknown as ExecutionContext;

const CHALLENGE_URL = "https://agentrelay.com/.well-known/openai-apps-challenge";

describe("OpenAI apps domain challenge", () => {
it("returns only the token as text/plain with a 200 when the secret is set", async () => {
const env = buildEnv({ OPENAI_APPS_CHALLENGE_TOKEN: TOKEN });
const response = await worker.fetch(new Request(CHALLENGE_URL), env, ctx);

expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/plain; charset=utf-8");
expect(response.headers.get("location")).toBeNull();
expect(await response.text()).toBe(TOKEN);
expect((env as unknown as { CLOUD_WEB_WORKER: { fetch: ReturnType<typeof vi.fn> } })
.CLOUD_WEB_WORKER.fetch).not.toHaveBeenCalled();
});

it("trims surrounding whitespace from the secret value", async () => {
const response = await worker.fetch(
new Request(CHALLENGE_URL),
buildEnv({ OPENAI_APPS_CHALLENGE_TOKEN: ` ${TOKEN}\n` }),
ctx,
);
expect(await response.text()).toBe(TOKEN);
});

it("answers HEAD with a 200 and no body", async () => {
const response = await worker.fetch(
new Request(CHALLENGE_URL, { method: "HEAD" }),
buildEnv({ OPENAI_APPS_CHALLENGE_TOKEN: TOKEN }),
ctx,
);
expect(response.status).toBe(200);
expect(await response.text()).toBe("");
});

it.each([undefined, "", " "])("returns 404 when the secret is %j", async (value) => {
const response = await worker.fetch(
new Request(CHALLENGE_URL),
buildEnv({ OPENAI_APPS_CHALLENGE_TOKEN: value }),
ctx,
);
expect(response.status).toBe(404);
expect(await response.text()).not.toContain(TOKEN);
});

it("rejects non-GET methods without leaking the token", async () => {
const response = await worker.fetch(
new Request(CHALLENGE_URL, { method: "POST" }),
buildEnv({ OPENAI_APPS_CHALLENGE_TOKEN: TOKEN }),
ctx,
);
expect(response.status).toBe(405);
expect(await response.text()).not.toContain(TOKEN);
});

it("only answers on the agentrelay.com apex", async () => {
const upstream = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("upstream", { status: 404 }));
const response = await worker.fetch(
new Request("https://arelay.to/.well-known/openai-apps-challenge"),
buildEnv({ OPENAI_APPS_CHALLENGE_TOKEN: TOKEN }),
ctx,
);
expect(await response.text()).not.toBe(TOKEN);
upstream.mockRestore();
});

it("does not serve the token on neighbouring paths", async () => {
const upstream = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("upstream", { status: 404 }));
for (const path of [
"/.well-known/openai-apps-challenge/",
"/.well-known/openai-apps-challenge/x",
"/cloud/.well-known/openai-apps-challenge",
]) {
const response = await worker.fetch(
new Request(`https://agentrelay.com${path}`),
buildEnv({ OPENAI_APPS_CHALLENGE_TOKEN: TOKEN }),
ctx,
);
expect(await response.text()).not.toBe(TOKEN);
}
upstream.mockRestore();
});
});
Loading