From 7bbf793ce615ee8c71047dc018f68a9b299b626f Mon Sep 17 00:00:00 2001
From: Luke Curley
Date: Sun, 16 Aug 2026 12:12:08 -0700
Subject: [PATCH] add Cloudflare relay shorthand
---
CLAUDE.md | 6 +++++-
README.md | 4 ++++
bun.lock | 5 +++++
package.json | 1 +
sites/lib/broadcast.test.ts | 36 ++++++++++++++++++++++++++++++++++++
sites/lib/broadcast.ts | 29 ++++++++++++++++++++++++++---
sites/pub/src/main.ts | 11 ++++++++---
sites/watch/src/main.ts | 10 ++++++++--
8 files changed, 93 insertions(+), 9 deletions(-)
create mode 100644 sites/lib/broadcast.test.ts
diff --git a/CLAUDE.md b/CLAUDE.md
index fef7768..76e0915 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -67,7 +67,11 @@ either site refers to the same broadcast — publish at `moq.pub/anon/x.hang` an
watch it back at `moq.watch/anon/x.hang`.
Anything that *isn't* part of the broadcast's identity stays in the query
-string: `?relay=`, `?jwt=`, and (moq.pub only) `?source=camera`.
+string: `?relay=`, `?cloudflare=`, `?jwt=`, and
+(moq.pub only) `?source=camera`. `?cloudflare=draft-16` is shorthand for
+`https://draft-16.cloudflare.mediaoverquic.com`; the project path segment still
+carries Cloudflare's relay token. `cloudflare` and `relay` are mutually
+exclusive.
**moq.pub deliberately doesn't link to moq.watch.** The path symmetry is the
feature; a link on top of it isn't. It also can't be built honestly: a link has
diff --git a/README.md b/README.md
index b2b9e42..16d6999 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,10 @@ The player sites use the path to name a broadcast: publish at
`moq.watch/anon/lazy-otter-4f21.hang`. Visiting [moq.pub](https://moq.pub) with
no path picks a random name for you.
+Use `?cloudflare=draft-16` to connect either player to
+`https://draft-16.cloudflare.mediaoverquic.com` without spelling out the full
+`?relay=` value. The first path segment remains the Cloudflare relay token.
+
These are clients only.
You'll either need to run a local server using [moq](https://github.com/moq-dev/moq) or use a public server such as `cdn.moq.pro`.
diff --git a/bun.lock b/bun.lock
index 05dbbbd..006adf0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -20,6 +20,7 @@
"@biomejs/biome": "^2.5.0",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.20",
+ "@types/bun": "^1.3.14",
"@types/node": "^22.19.21",
"tailwindcss": "^3.4.19",
"vite": "^6.4.1",
@@ -361,6 +362,8 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
+
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@@ -439,6 +442,8 @@
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
+
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
diff --git a/package.json b/package.json
index 651368c..6665138 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"@biomejs/biome": "^2.5.0",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.20",
+ "@types/bun": "^1.3.14",
"@types/node": "^22.19.21",
"tailwindcss": "^3.4.19",
"vite": "^6.4.1",
diff --git a/sites/lib/broadcast.test.ts b/sites/lib/broadcast.test.ts
new file mode 100644
index 0000000..7890eb7
--- /dev/null
+++ b/sites/lib/broadcast.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, test } from "bun:test";
+import { relay } from "./broadcast";
+
+const broadcast = { project: "cloudflare-token", name: "room/camera.hang" };
+const fallback = "https://cdn.moq.pro";
+
+describe("relay", () => {
+ test("uses the default relay", () => {
+ expect(relay(broadcast, new URLSearchParams(), fallback)?.toString()).toBe("https://cdn.moq.pro/cloudflare-token");
+ });
+
+ test("builds a Cloudflare relay from one subdomain label", () => {
+ const params = new URLSearchParams({ cloudflare: "draft-16" });
+ expect(relay(broadcast, params, fallback)?.toString()).toBe(
+ "https://draft-16.cloudflare.mediaoverquic.com/cloudflare-token",
+ );
+ });
+
+ test("rejects malformed Cloudflare labels", () => {
+ for (const value of ["", "Draft-16", ".draft-16", "draft-16.example", "-draft-16", "draft-16-"]) {
+ expect(relay(broadcast, new URLSearchParams({ cloudflare: value }), fallback)).toBeUndefined();
+ }
+ });
+
+ test("rejects conflicting relay selectors", () => {
+ const params = new URLSearchParams({ cloudflare: "draft-16", relay: "relay.example.com" });
+ expect(relay(broadcast, params, fallback)).toBeUndefined();
+ });
+
+ test("preserves explicit relay and JWT support", () => {
+ const params = new URLSearchParams({ relay: "http://localhost:4443/base", jwt: "token" });
+ expect(relay(broadcast, params, fallback)?.toString()).toBe(
+ "http://localhost:4443/base/cloudflare-token?jwt=token",
+ );
+ });
+});
diff --git a/sites/lib/broadcast.ts b/sites/lib/broadcast.ts
index f615d8b..22e9dba 100644
--- a/sites/lib/broadcast.ts
+++ b/sites/lib/broadcast.ts
@@ -15,6 +15,9 @@ export interface Broadcast {
name: string;
}
+const CLOUDFLARE_RELAY_DOMAIN = "cloudflare.mediaoverquic.com";
+const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
+
/** Parse `//`, or undefined when the path isn't a broadcast. */
export function parse(pathname: string): Broadcast | undefined {
const raw = pathname.split("/").filter((part) => part !== "");
@@ -44,10 +47,30 @@ export function path({ project, name }: Broadcast): string {
/**
* The relay URL to connect to: `/`, carrying `?jwt=` when the
- * page was given a token.
+ * page was given a token. `?cloudflare=
Optional:
?relay=https://cdn.moq.pro
+ ?cloudflare=draft-16
?jwt=<token>
`;
}