From e19e61ffbab92e20e3dd5e822b0a247b8aa12d1a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 5 Aug 2026 09:01:03 -0700 Subject: [PATCH 1/4] Move moq.pub and moq.watch into the repo, keyed by path The two player sites lived in a separate repo and deployed by hand. Bring them in as sites/pub and sites/watch, each still its own Worker but built and uploaded by the same `just deploy`, and reading the repo-root .env files so PUBLIC_RELAY_URL is the single place the relay is configured. The old copies hardcoded cdn.moq.dev, stale since the move to cdn.moq.pro. Broadcasts are now named by the path rather than the query string: //, e.g. moq.pub/anon/lazy-otter-4f21.hang. The same path on moq.watch plays it back, so a publisher can share its own URL with one substitution, and the publisher page links to it. A bare moq.pub/ redirects to an invented name server-side, so the shareable URL exists before any JS runs. Old ?project=&name= links redirect into the path. Everything that isn't part of a broadcast's identity - relay, jwt, source - stays in the query string. sites/lib holds the scheme once, shared by both sites and by both the Worker and the Vite dev server so the two can't drift. Two things this had to work around: - assets.run_worker_first is load-bearing. `/` matches index.html, so without it Cloudflare's asset server answers first and the Worker never runs, meaning no redirect and no invented name. - The Worker can't tell an asset from a page by looking for a dot, since names end in .hang. It asks the asset store and falls back to the page on a 404. Declaring vite as a direct devDependency (it was only reachable through astro) re-hoists esbuild in the lockfile: vite wants ^0.25.0 and astro wants ^0.27.3, so they were always on separate copies and each still resolves to the same version as before. Only the tree position swapped. Co-Authored-By: Claude Opus 5 --- .env.development | 1 + .env.live | 1 + .env.staging | 1 + CLAUDE.md | 52 ++++++- README.md | 29 +++- bun.lock | 163 ++++++++++++------- justfile | 19 ++- package.json | 3 + sites/env.d.ts | 8 + sites/lib/broadcast.ts | 50 ++++++ sites/lib/dev.ts | 34 ++++ sites/lib/name.ts | 15 ++ sites/lib/route.ts | 41 +++++ sites/lib/worker.ts | 35 +++++ sites/pub/index.html | 70 +++++++++ sites/pub/public/favicon.png | Bin 0 -> 34089 bytes sites/pub/public/favicon.svg | 11 ++ sites/pub/public/logo.svg | 268 ++++++++++++++++++++++++++++++++ sites/pub/public/wordmark.svg | 140 +++++++++++++++++ sites/pub/src/main.ts | 58 +++++++ sites/pub/src/worker.ts | 7 + sites/pub/vite.config.ts | 22 +++ sites/pub/wrangler.jsonc | 43 +++++ sites/watch/index.html | 90 +++++++++++ sites/watch/public/favicon.png | Bin 0 -> 34089 bytes sites/watch/public/favicon.svg | 11 ++ sites/watch/public/logo.svg | 268 ++++++++++++++++++++++++++++++++ sites/watch/public/wordmark.svg | 107 +++++++++++++ sites/watch/src/main.ts | 53 +++++++ sites/watch/src/worker.ts | 7 + sites/watch/vite.config.ts | 22 +++ sites/watch/wrangler.jsonc | 43 +++++ 32 files changed, 1607 insertions(+), 65 deletions(-) create mode 100644 sites/env.d.ts create mode 100644 sites/lib/broadcast.ts create mode 100644 sites/lib/dev.ts create mode 100644 sites/lib/name.ts create mode 100644 sites/lib/route.ts create mode 100644 sites/lib/worker.ts create mode 100644 sites/pub/index.html create mode 100644 sites/pub/public/favicon.png create mode 100644 sites/pub/public/favicon.svg create mode 100644 sites/pub/public/logo.svg create mode 100644 sites/pub/public/wordmark.svg create mode 100644 sites/pub/src/main.ts create mode 100644 sites/pub/src/worker.ts create mode 100644 sites/pub/vite.config.ts create mode 100644 sites/pub/wrangler.jsonc create mode 100644 sites/watch/index.html create mode 100644 sites/watch/public/favicon.png create mode 100644 sites/watch/public/favicon.svg create mode 100644 sites/watch/public/logo.svg create mode 100644 sites/watch/public/wordmark.svg create mode 100644 sites/watch/src/main.ts create mode 100644 sites/watch/src/worker.ts create mode 100644 sites/watch/vite.config.ts create mode 100644 sites/watch/wrangler.jsonc diff --git a/.env.development b/.env.development index fb4f66a..ce17928 100644 --- a/.env.development +++ b/.env.development @@ -1 +1,2 @@ PUBLIC_RELAY_URL="http://localhost:4443" +PUBLIC_WATCH_URL="http://localhost:5173" diff --git a/.env.live b/.env.live index 5b0fc22..5476a62 100644 --- a/.env.live +++ b/.env.live @@ -1,2 +1,3 @@ PUBLIC_RELAY_URL="https://cdn.moq.pro" PUBLIC_CLOUDFLARE_URL="https://relay.cloudflare.mediaoverquic.com" +PUBLIC_WATCH_URL="https://moq.watch" diff --git a/.env.staging b/.env.staging index a631747..5b81e2e 100644 --- a/.env.staging +++ b/.env.staging @@ -1,2 +1,3 @@ PUBLIC_RELAY_URL="https://cdn.moq.wtf" PUBLIC_CLOUDFLARE_URL="https://relay.cloudflare.mediaoverquic.com" +PUBLIC_WATCH_URL="https://new.moq.watch" diff --git a/CLAUDE.md b/CLAUDE.md index 8707c95..3a4488a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,15 +6,25 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co moq.dev is a web blog and demo for Media over QUIC (MoQ) protocol. It's built with Astro, Solid.js, and uses WebTransport to connect to MoQ relay servers for live streaming. +The repo holds three sites, each its own Cloudflare Worker but built and deployed together: + +| Site | Source | What it is | +|-------------|-------------|-------------------------------------------------------| +| `moq.dev` | `src/` | The Astro blog and demos | +| `moq.pub` | `sites/pub` | A bare `` publisher, no framework | +| `moq.watch` | `sites/watch` | A bare `` player, no framework | + ## Essential Commands ```bash # Development -just dev # Start dev server with auto-open +just dev # Start the moq.dev dev server with auto-open +just dev-pub # moq.pub on :5174 +just dev-watch # moq.watch on :5173 # Build & Deploy -just build # Production build -just deploy # Deploy to Cloudflare Pages (staging by default) +just build # Production build of all three sites +just deploy # Deploy all three to Cloudflare (staging by default) just deploy live # Deploy to production, and email subscribers about new posts just prod # Build and preview production locally @@ -48,12 +58,45 @@ just fix # Auto-fix code formatting/lint issues - **Authentication**: Basic JWT support via query parameters for demo broadcasts - **Content Management**: MDX files in `src/pages/blog/` for documentation +### The moq.pub / moq.watch URL scheme + +A broadcast is identified by its path: `//`, e.g. +`moq.pub/anon/lazy-otter-4f21.hang`. The project is the relay tenant and the +name is everything after it, so names may contain slashes. The same path on +either site refers to the same broadcast — publish at `moq.pub/anon/x.hang` and +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`. + +`sites/lib` holds the scheme itself, shared by both sites and by both the Worker +and the Vite dev server so they can't drift: + +- `broadcast.ts` — parse and build `//`, and the relay URL +- `route.ts` — which requests redirect: a bare `moq.pub/` invents a random name, + and old `?project=&name=` links move into the path +- `worker.ts` — the Worker both sites export +- `dev.ts` — a Vite plugin giving `bun run dev` the same routing + +Two things are easy to break here: + +- `assets.run_worker_first: ["/"]` in each `wrangler.jsonc` is load-bearing. `/` + matches `index.html`, so without it Cloudflare's asset server answers first and + the Worker never runs — no redirect, no invented name. +- The Worker can't decide "asset vs. page" by looking for a dot, because names + end in `.hang`. It asks the asset store and falls back to the page on a 404. + ### Deployment -- Cloudflare Pages via Wrangler +- Cloudflare Workers via Wrangler, one per site - `just deploy` for staging, `just deploy live` for production - Deploys are manual; nothing ships on merge to `main` +`just deploy ` builds all three sites in that mode and uploads each Worker. +The player sites read the repo-root `.env.` files that the Astro site uses, +so `PUBLIC_RELAY_URL` is the single place the relay is configured. Staging is +`new.moq.dev`, `new.moq.pub`, and `new.moq.watch`. + **`just deploy live` mails the subscriber list.** `scripts/notify-subscribers.ts` snapshots the slugs in `https://moq.dev/rss.xml` before the upload, then sends a Resend broadcast for every post in the freshly built `dist/rss.xml` that wasn't in that snapshot. Subject and body come from the feed's `title` and `description`. A deploy that adds no posts sends nothing. Credentials come from 1Password, so no secret has to sit on disk. `op.env` maps `RESEND_API_KEY` to `op://Corp/Resend/credential` and `op run` resolves it for the duration of the command. That file is committed on purpose: it holds references, not values. Install and sign in once with `brew install 1password-cli && op signin`. @@ -68,3 +111,4 @@ Broadcasts cannot be recalled, so the script refuses to guess: an unreachable or - The `@moq/publish` and `@moq/watch` packages handle all MoQ protocol implementation - For new blog posts, add MDX files to `src/pages/blog/` - Component changes in `src/components/` automatically reload with HMR +- moq.pub links to moq.watch, so `just dev-pub` expects `just dev-watch` on :5173 diff --git a/README.md b/README.md index 2bf38d8..b2b9e42 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,46 @@ Media over QUIC

-This repository contains the code for [moq.dev](https://moq.dev). +This repository contains the code for three sites: -This is a client only. +- [moq.dev](https://moq.dev) — the blog and demos, in `src/` +- [moq.pub](https://moq.pub) — a bare-bones publisher, in `sites/pub` +- [moq.watch](https://moq.watch) — a bare-bones player, in `sites/watch` + +The player sites use the path to name a broadcast: publish at +`moq.pub/anon/lazy-otter-4f21.hang` and watch it back at +`moq.watch/anon/lazy-otter-4f21.hang`. Visiting [moq.pub](https://moq.pub) with +no path picks a random name for you. + +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`. Join the [Discord](https://discord.moq.dev) for updates and discussion. ## Setup -Install the dependencies with `npm`: +Install the dependencies with `bun`: ```bash -npm i +bun i ``` ## Development -Run the development web server: +Run a development web server: ```bash -npm run dev +just dev # moq.dev +just dev-pub # moq.pub, on :5174 +just dev-watch # moq.watch, on :5173 ``` +## Deploy + +`just deploy` builds and uploads all three sites to Cloudflare, staging by +default; `just deploy live` goes to production and mails subscribers about any +new blog posts. + ## License Licensed under either: diff --git a/bun.lock b/bun.lock index 3469fd7..05dbbbd 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,7 @@ "@tailwindcss/typography": "^0.5.20", "@types/node": "^22.19.21", "tailwindcss": "^3.4.19", + "vite": "^6.4.1", "vite-plugin-static-copy": "^2.3.2", "wrangler": "^4.100.0", }, @@ -122,57 +123,57 @@ "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], @@ -540,7 +541,7 @@ "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -1176,6 +1177,8 @@ "astro/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.11", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ=="], + "astro/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], @@ -1208,7 +1211,7 @@ "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1228,61 +1231,113 @@ "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "astro/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "astro/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "astro/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "astro/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "astro/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "astro/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "astro/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "astro/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "astro/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "astro/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "astro/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "astro/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "astro/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "astro/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "astro/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "astro/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "astro/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "astro/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "astro/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "astro/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "astro/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "astro/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "astro/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "astro/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "astro/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + "wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } diff --git a/justfile b/justfile index 19e8359..ed1a031 100644 --- a/justfile +++ b/justfile @@ -40,12 +40,18 @@ build mode="live": bun i bun astro build --mode {{mode}} -# Deploy the site to Cloudflare Pages + # The moq.pub and moq.watch player sites, which share these .env files. + bun vite build sites/pub --mode {{mode}} + bun vite build sites/watch --mode {{mode}} + +# Deploy all three sites to Cloudflare # On `live`, any post that wasn't already on moq.dev gets mailed to subscribers. deploy env="staging": (build env) # Record what's live before we replace it, so we can tell what the deploy added. bun scripts/notify-subscribers.ts snapshot --env {{env}} bun wrangler deploy --env {{env}} + bun wrangler deploy --config sites/pub/wrangler.jsonc --env {{env}} + bun wrangler deploy --config sites/watch/wrangler.jsonc --env {{env}} just _announce {{env}} # Mail subscribers about anything this deploy published. @@ -75,5 +81,16 @@ dev: # Run the web development server bun astro dev --open +# Run the moq.pub development server on :5174. It links broadcasts to :5173, +# so run `just dev-watch` alongside it to follow those links. +dev-pub: + bun i + bun vite sites/pub --open + +# Run the moq.watch development server on :5173. +dev-watch: + bun i + bun vite sites/watch --open + prod: (build "live") bun astro preview --open diff --git a/package.json b/package.json index d7048b8..651368c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "private": true, "scripts": { "dev": "astro dev --open", + "dev:pub": "vite sites/pub", + "dev:watch": "vite sites/watch", "build": "astro build --production", "preview": "wrangler pages dev ./dist", "deploy": "wrangler deploy", @@ -31,6 +33,7 @@ "@tailwindcss/typography": "^0.5.20", "@types/node": "^22.19.21", "tailwindcss": "^3.4.19", + "vite": "^6.4.1", "vite-plugin-static-copy": "^2.3.2", "wrangler": "^4.100.0" }, diff --git a/sites/env.d.ts b/sites/env.d.ts new file mode 100644 index 0000000..7852afe --- /dev/null +++ b/sites/env.d.ts @@ -0,0 +1,8 @@ +// The moq.pub and moq.watch builds read the repo-root .env files, so they see +// the same PUBLIC_ variables the Astro site does. +interface ImportMetaEnv { + /** The relay to connect to, e.g. https://cdn.moq.pro. */ + readonly PUBLIC_RELAY_URL: string; + /** Where moq.watch is served, used to link a broadcast to its playback URL. */ + readonly PUBLIC_WATCH_URL: string; +} diff --git a/sites/lib/broadcast.ts b/sites/lib/broadcast.ts new file mode 100644 index 0000000..e786f49 --- /dev/null +++ b/sites/lib/broadcast.ts @@ -0,0 +1,50 @@ +/** + * The URL scheme shared by moq.pub and moq.watch: a broadcast lives at + * `//`. + * + * The project is the relay tenant (`anon`, `demo`, ...) and the name is + * everything after it, so names may contain slashes. Anything that isn't part + * of the broadcast's identity — the relay host, an auth token, the capture + * source — stays in the query string. + */ + +export interface Broadcast { + /** The relay project: the first path segment. */ + project: string; + /** The broadcast name: everything after the project. */ + name: string; +} + +/** Parse `//`, or undefined when the path isn't a broadcast. */ +export function parse(pathname: string): Broadcast | undefined { + const parts = pathname + .split("/") + .filter((part) => part !== "") + .map(decodeURIComponent); + + // A project on its own isn't enough to publish or watch anything. + if (parts.length < 2) return undefined; + + const [project, ...name] = parts; + return { project, name: name.join("/") }; +} + +/** The inverse of {@link parse}: the path a broadcast lives at. */ +export function path({ project, name }: Broadcast): string { + const parts = [project, ...name.split("/")].filter((part) => part !== ""); + return `/${parts.map(encodeURIComponent).join("/")}`; +} + +/** + * The relay URL to connect to: `/`, carrying `?jwt=` when the + * page was given a token. + */ +export function relay(broadcast: Broadcast, params: URLSearchParams, fallback: string): URL { + const url = new URL(params.get("relay") || fallback); + url.pathname = `${url.pathname.replace(/\/+$/, "")}/${broadcast.project}`; + + const jwt = params.get("jwt"); + if (jwt) url.searchParams.set("jwt", jwt); + + return url; +} diff --git a/sites/lib/dev.ts b/sites/lib/dev.ts new file mode 100644 index 0000000..72953d4 --- /dev/null +++ b/sites/lib/dev.ts @@ -0,0 +1,34 @@ +/** + * Teach the Vite dev server the routing that the Worker does in production, so + * `/anon/foo.hang` loads the page locally instead of 404ing. + */ +import type { Plugin } from "vite"; +import { type Options, redirect } from "./route"; + +export function routing(options: Options): Plugin { + return { + name: "moq-routing", + configureServer(server) { + server.middlewares.use((req, res, next) => { + // Only navigations are ours. Module and asset requests have to reach + // Vite untouched, or `/logo.svg` gets mistaken for a broadcast path and + // redirected. In production that split is `run_worker_first`. + if (!req.headers.accept?.includes("text/html")) return next(); + + const url = new URL(req.url ?? "/", "http://localhost"); + + const location = redirect(url, options); + if (location) { + res.writeHead(302, { Location: location, "Cache-Control": "no-store" }); + res.end(); + return; + } + + // Serve the page for whatever path this is. The browser keeps the real + // URL, which is what the page reads. + req.url = "/"; + next(); + }); + }, + }; +} diff --git a/sites/lib/name.ts b/sites/lib/name.ts new file mode 100644 index 0000000..4287c46 --- /dev/null +++ b/sites/lib/name.ts @@ -0,0 +1,15 @@ +/** + * Random broadcast names for moq.pub. + * + * Anyone can publish to the `anon` project, so the name we invent has to be + * unlikely to collide with a stranger's. Two dictionary words keep the URL + * readable enough to say out loud; the random suffix is what actually makes a + * collision negligible. + */ +import { adjectives, animals, uniqueNamesGenerator } from "unique-names-generator"; + +export function random(): string { + const words = uniqueNamesGenerator({ dictionaries: [adjectives, animals], separator: "-" }); + const suffix = crypto.getRandomValues(new Uint16Array(1))[0].toString(16).padStart(4, "0"); + return `${words}-${suffix}.hang`; +} diff --git a/sites/lib/route.ts b/sites/lib/route.ts new file mode 100644 index 0000000..11f9a19 --- /dev/null +++ b/sites/lib/route.ts @@ -0,0 +1,41 @@ +/** + * Routing shared by the Workers and the Vite dev server, so `bun run dev` and + * production agree on what a URL means. + * + * Two cases redirect before the page ever loads: + * + * - moq.pub with no broadcast in the path invents one, so the publisher has a + * stable, shareable URL from the first paint rather than after the JS runs. + * - the old `?project=&name=` links, which now live in the path. + */ +import * as Broadcast from "./broadcast"; +import { random } from "./name"; + +export interface Options { + /** Invent a broadcast name when the request doesn't carry one (moq.pub). */ + invent: boolean; + /** The project to assume when the URL doesn't name one. */ + project: string; +} + +/** The path to redirect to, or undefined to serve the request as-is. */ +export function redirect(url: URL, options: Options): string | undefined { + // Already a broadcast path; nothing to do. + if (Broadcast.parse(url.pathname)) return undefined; + + const params = new URLSearchParams(url.search); + const name = params.get("name") ?? (options.invent ? random() : undefined); + + // moq.watch with nothing to watch: let the page show its usage hint. + if (!name) return undefined; + + const project = params.get("project") || options.project; + + // The rest of the query string (relay, jwt, source) isn't part of the + // broadcast's identity, so it survives the move into the path. + params.delete("name"); + params.delete("project"); + const query = params.toString(); + + return Broadcast.path({ project, name }) + (query ? `?${query}` : ""); +} diff --git a/sites/lib/worker.ts b/sites/lib/worker.ts new file mode 100644 index 0000000..5af76de --- /dev/null +++ b/sites/lib/worker.ts @@ -0,0 +1,35 @@ +/** + * The Cloudflare Worker behind moq.pub and moq.watch: serve the Vite build, and + * hand every broadcast path to the single page that renders it. + */ +import { type Options, redirect } from "./route"; + +interface Env { + ASSETS: { fetch: (request: Request) => Promise }; +} + +export function handler(options: Options) { + return { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + const location = redirect(url, options); + if (location) { + // An invented name is different on every request, so this redirect must + // not be cached anywhere between here and the browser. + return new Response(null, { + status: 302, + headers: { Location: location, "Cache-Control": "no-store" }, + }); + } + + // Broadcast names end in `.hang`, so "does the path look like a file?" + // can't tell a page apart from an asset. Ask the asset store instead, and + // fall back to the page, which reads the path client-side. + const asset = await env.ASSETS.fetch(request); + if (asset.status !== 404) return asset; + + return env.ASSETS.fetch(new Request(new URL("/", url), { headers: request.headers })); + }, + }; +} diff --git a/sites/pub/index.html b/sites/pub/index.html new file mode 100644 index 0000000..a253df5 --- /dev/null +++ b/sites/pub/index.html @@ -0,0 +1,70 @@ + + + + + + moq.pub + + + + + + + + + diff --git a/sites/pub/public/favicon.png b/sites/pub/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..14ff9c03fbd64d53a15d1ece9da9ae298e282cf6 GIT binary patch literal 34089 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

M-dpVR^U$EWFD_al_uH=6=DhZpmF8Ns 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 + + + + + + + diff --git a/sites/pub/public/logo.svg b/sites/pub/public/logo.svg new file mode 100644 index 0000000..8f2e30a --- /dev/null +++ b/sites/pub/public/logo.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + diff --git a/sites/pub/public/wordmark.svg b/sites/pub/public/wordmark.svg new file mode 100644 index 0000000..e6d4bad --- /dev/null +++ b/sites/pub/public/wordmark.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + diff --git a/sites/pub/src/main.ts b/sites/pub/src/main.ts new file mode 100644 index 0000000..6a06c2d --- /dev/null +++ b/sites/pub/src/main.ts @@ -0,0 +1,58 @@ +// moq.pub — publish the broadcast named by the path, e.g. /anon/lazy-otter-4f21.hang. +// +// The Worker redirects anything else to a broadcast path, inventing a name when +// the URL doesn't carry one, so this page always loads on the shareable URL. +// +// Everything that isn't part of the broadcast's identity stays in the query: +// ?relay= Relay server URL (default: the relay this site was built for) +// ?jwt= Appended to the relay URL as ?jwt= +// ?source= Preselect a capture source: camera, screen, or file +import "@moq/publish/element"; +import "@moq/publish/ui"; + +import * as Broadcast from "../../lib/broadcast"; + +const DEFAULT_RELAY = import.meta.env.PUBLIC_RELAY_URL ?? "https://cdn.moq.pro"; +const WATCH_URL = import.meta.env.PUBLIC_WATCH_URL ?? "https://moq.watch"; + +const broadcast = Broadcast.parse(location.pathname); +if (broadcast) { + mount(broadcast); +} else { + // Shouldn't happen, since the Worker redirects everything else here. Bounce + // off the root and let it invent a name rather than show a blank page. + location.replace("/"); +} + +function mount(broadcast: Broadcast.Broadcast) { + const params = new URLSearchParams(location.search); + const relay = Broadcast.relay(broadcast, params, DEFAULT_RELAY); + + // The same path on moq.watch plays this broadcast back. + const share = document.createElement("a"); + share.className = "share"; + share.target = "_blank"; + share.rel = "noreferrer"; + share.href = new URL(Broadcast.path(broadcast), WATCH_URL).toString(); + share.textContent = share.href.replace(/^https?:\/\//, ""); + + const publish = document.createElement("moq-publish"); + publish.setAttribute("url", relay.toString()); + publish.setAttribute("name", broadcast.name); + + // Optional: preselect a source (camera/screen/file). Otherwise the UI lets + // the user pick one before anything is published. + const source = params.get("source"); + if (source) publish.setAttribute("source", source); + + const video = document.createElement("video"); + video.muted = true; + video.autoplay = true; + video.setAttribute("playsinline", ""); + publish.appendChild(video); + + const ui = document.createElement("moq-publish-ui"); + ui.appendChild(publish); + + document.body.append(share, ui); +} diff --git a/sites/pub/src/worker.ts b/sites/pub/src/worker.ts new file mode 100644 index 0000000..28a2249 --- /dev/null +++ b/sites/pub/src/worker.ts @@ -0,0 +1,7 @@ +/** + * moq.pub — serves the Vite build, and redirects a bare `/` to a freshly + * invented broadcast so the publisher lands on a shareable URL. + */ +import { handler } from "../../lib/worker"; + +export default handler({ invent: true, project: "anon" }); diff --git a/sites/pub/vite.config.ts b/sites/pub/vite.config.ts new file mode 100644 index 0000000..b11ea41 --- /dev/null +++ b/sites/pub/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vite"; +import { routing } from "../lib/dev"; + +// moq.pub — a static Vite build of the @moq/publish player. The Worker in +// src/worker.ts serves the dist/ output. +export default defineConfig({ + // Share the repo-root .env files with the Astro site, which means Astro's + // PUBLIC_ prefix rather than Vite's default VITE_. + envDir: "../..", + envPrefix: "PUBLIC_", + plugins: [routing({ invent: true, project: "anon" })], + build: { + target: "esnext", + outDir: "dist", + emptyOutDir: true, + }, + server: { + port: 5174, + // sites/lib is shared with moq.watch, so it lives outside this root. + fs: { allow: ["../.."] }, + }, +}); diff --git a/sites/pub/wrangler.jsonc b/sites/pub/wrangler.jsonc new file mode 100644 index 0000000..687ec84 --- /dev/null +++ b/sites/pub/wrangler.jsonc @@ -0,0 +1,43 @@ +{ + // Check the configuration file schema, the main reason why we're not using TOML. + "$schema": "../../node_modules/wrangler/config-schema.json", + + // Default configuration for local development + "name": "moq-pub-local", + "compatibility_date": "2025-08-13", + "account_id": "dd618f5dbd5da77b8296f1613c301f5c", + + "main": "src/worker.ts", + + "assets": { + "directory": "./dist", + "binding": "ASSETS", + + // `/` matches index.html, so without this the asset server answers it and the + // Worker never gets to invent a broadcast and redirect. Every other path + // misses the assets and falls through to the Worker on its own. + "run_worker_first": ["/"] + }, + + "observability": { + "enabled": true + }, + + // Environment-specific configurations + "env": { + "staging": { + "name": "moq-pub-staging", + "route": { + "pattern": "new.moq.pub", + "custom_domain": true + } + }, + "live": { + "name": "moq-pub", + "route": { + "pattern": "moq.pub", + "custom_domain": true + } + } + } +} diff --git a/sites/watch/index.html b/sites/watch/index.html new file mode 100644 index 0000000..e7002b2 --- /dev/null +++ b/sites/watch/index.html @@ -0,0 +1,90 @@ + + + + + + moq.watch + + + + + + + + + diff --git a/sites/watch/public/favicon.png b/sites/watch/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..14ff9c03fbd64d53a15d1ece9da9ae298e282cf6 GIT binary patch literal 34089 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

M-dpVR^U$EWFD_al_uH=6=DhZpmF8Ns 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 + + + + + + + diff --git a/sites/watch/public/logo.svg b/sites/watch/public/logo.svg new file mode 100644 index 0000000..8f2e30a --- /dev/null +++ b/sites/watch/public/logo.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + diff --git a/sites/watch/public/wordmark.svg b/sites/watch/public/wordmark.svg new file mode 100644 index 0000000..06ffa00 --- /dev/null +++ b/sites/watch/public/wordmark.svg @@ -0,0 +1,107 @@ + + + + + + + + diff --git a/sites/watch/src/main.ts b/sites/watch/src/main.ts new file mode 100644 index 0000000..2585502 --- /dev/null +++ b/sites/watch/src/main.ts @@ -0,0 +1,53 @@ +// moq.watch — play the broadcast named by the path, e.g. /demo/bbb.hang. +// +// Everything that isn't part of the broadcast's identity stays in the query: +// ?relay= Relay server URL (default: the relay this site was built for) +// ?jwt= Appended to the relay URL as ?jwt= +import "@moq/watch/element"; +import "@moq/watch/ui"; + +import * as Broadcast from "../../lib/broadcast"; + +const DEFAULT_RELAY = import.meta.env.PUBLIC_RELAY_URL ?? "https://cdn.moq.pro"; + +const broadcast = Broadcast.parse(location.pathname); +if (broadcast) { + mount(broadcast); +} else { + usage(); +} + +function mount(broadcast: Broadcast.Broadcast) { + const params = new URLSearchParams(location.search); + const relay = Broadcast.relay(broadcast, params, DEFAULT_RELAY); + + const watch = document.createElement("moq-watch"); + watch.setAttribute("url", relay.toString()); + watch.setAttribute("name", broadcast.name); + watch.setAttribute("reload", ""); + watch.appendChild(document.createElement("canvas")); + + const ui = document.createElement("moq-watch-ui"); + ui.appendChild(watch); + + document.body.appendChild(ui); +} + +// Nothing in the path to watch, so say what one looks like. +function usage() { + document.body.innerHTML = ` +

+
+ + watch +
+

Watch a Media over QUIC broadcast.

+

Put the project and broadcast in the path:
+ /demo/bbb.hang

+

Publish your own at moq.pub, then watch + it back at the same path here.

+

Optional:
+ ?relay=https://cdn.moq.pro   + ?jwt=<token>

+
`; +} diff --git a/sites/watch/src/worker.ts b/sites/watch/src/worker.ts new file mode 100644 index 0000000..a50907a --- /dev/null +++ b/sites/watch/src/worker.ts @@ -0,0 +1,7 @@ +/** + * moq.watch — serves the Vite build. Unlike moq.pub it never invents a + * broadcast: a bare `/` gets the usage hint instead. + */ +import { handler } from "../../lib/worker"; + +export default handler({ invent: false, project: "demo" }); diff --git a/sites/watch/vite.config.ts b/sites/watch/vite.config.ts new file mode 100644 index 0000000..259910e --- /dev/null +++ b/sites/watch/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vite"; +import { routing } from "../lib/dev"; + +// moq.watch — a static Vite build of the @moq/watch player. The Worker in +// src/worker.ts serves the dist/ output. +export default defineConfig({ + // Share the repo-root .env files with the Astro site, which means Astro's + // PUBLIC_ prefix rather than Vite's default VITE_. + envDir: "../..", + envPrefix: "PUBLIC_", + plugins: [routing({ invent: false, project: "demo" })], + build: { + target: "esnext", + outDir: "dist", + emptyOutDir: true, + }, + server: { + port: 5173, + // sites/lib is shared with moq.pub, so it lives outside this root. + fs: { allow: ["../.."] }, + }, +}); diff --git a/sites/watch/wrangler.jsonc b/sites/watch/wrangler.jsonc new file mode 100644 index 0000000..fd763ac --- /dev/null +++ b/sites/watch/wrangler.jsonc @@ -0,0 +1,43 @@ +{ + // Check the configuration file schema, the main reason why we're not using TOML. + "$schema": "../../node_modules/wrangler/config-schema.json", + + // Default configuration for local development + "name": "moq-watch-local", + "compatibility_date": "2025-08-13", + "account_id": "dd618f5dbd5da77b8296f1613c301f5c", + + "main": "src/worker.ts", + + "assets": { + "directory": "./dist", + "binding": "ASSETS", + + // `/` matches index.html, so without this the asset server answers it and the + // Worker never gets to redirect the old `?name=` links. Every other path + // misses the assets and falls through to the Worker on its own. + "run_worker_first": ["/"] + }, + + "observability": { + "enabled": true + }, + + // Environment-specific configurations + "env": { + "staging": { + "name": "moq-watch-staging", + "route": { + "pattern": "new.moq.watch", + "custom_domain": true + } + }, + "live": { + "name": "moq-watch", + "route": { + "pattern": "moq.watch", + "custom_domain": true + } + } + } +} From 295b56135fc58336e1977fa540ec2aec01afec44 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 5 Aug 2026 09:11:10 -0700 Subject: [PATCH 2/4] Carry relay into the watch link, keep the publish token out The share link on moq.pub was built from the broadcast path alone, so a broadcast published to a non-default relay produced a link that opened moq.watch against the wrong relay and found nothing. `relay` now carries over. `jwt` deliberately does not: moq tokens are prefix-scoped with separate publish (`put`) and subscribe (`get`) grants, so a token being used to publish carries `put`, and copying it into a link meant to be passed around would hand every recipient the right to publish rather than just to watch. Reviewers suggested carrying the whole query; that trades a broken link for a credential leak. Instead, a publisher who wants to share access to a private broadcast passes a subscribe-only token as `?viewer=`, which becomes the `?jwt=` on the moq.watch end. With a `jwt` and no `viewer` the page says viewers need their own token, so the remaining gap is visible rather than looking like a working link that silently won't connect. Also deploy the player sites before moq.dev. Snapshot, moq.dev upload and announce are effectively a transaction: a failure in the middle leaves posts live but unannounced, and the snapshot expires after an hour, so a later retry reads those posts as already-published and never mails them. The two new deploys sat inside that window; now they run ahead of it. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 18 +++++++++++++++++- justfile | 10 ++++++++-- sites/lib/broadcast.ts | 25 +++++++++++++++++++++++++ sites/pub/index.html | 11 ++++++++++- sites/pub/src/main.ts | 18 ++++++++++++++++-- 5 files changed, 76 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3a4488a..8773288 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,17 @@ 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=`, `?jwt=`, and (moq.pub only) `?source=camera` and +`?viewer=`. + +**`jwt` is never copied into the moq.watch link that moq.pub shows.** moq tokens +are prefix-scoped with separate publish (`put`) and subscribe (`get`) grants, so +a token being used to publish carries `put` — putting it in a link meant to be +passed around would hand every recipient the right to publish. `relay` does +carry over, since a viewer on the wrong relay finds nothing. To share access to +a private broadcast, pass a subscribe-only token as `?viewer=`; it becomes the +`?jwt=` on the moq.watch end. With a `jwt` and no `viewer`, the page says so +rather than showing a link that looks fine and won't connect. `sites/lib` holds the scheme itself, shared by both sites and by both the Worker and the Vite dev server so they can't drift: @@ -97,6 +107,12 @@ The player sites read the repo-root `.env.` files that the Astro site uses, so `PUBLIC_RELAY_URL` is the single place the relay is configured. Staging is `new.moq.dev`, `new.moq.pub`, and `new.moq.watch`. +The player sites deploy *before* moq.dev on purpose. Snapshot → moq.dev upload → +announce is effectively a transaction: a failure in the middle leaves posts live +but unannounced, and since the snapshot expires after an hour, a later retry +reads those posts as already-published and never mails them. Keep anything +fallible out from between those three steps. + **`just deploy live` mails the subscriber list.** `scripts/notify-subscribers.ts` snapshots the slugs in `https://moq.dev/rss.xml` before the upload, then sends a Resend broadcast for every post in the freshly built `dist/rss.xml` that wasn't in that snapshot. Subject and body come from the feed's `title` and `description`. A deploy that adds no posts sends nothing. Credentials come from 1Password, so no secret has to sit on disk. `op.env` maps `RESEND_API_KEY` to `op://Corp/Resend/credential` and `op run` resolves it for the duration of the command. That file is committed on purpose: it holds references, not values. Install and sign in once with `brew install 1password-cli && op signin`. diff --git a/justfile b/justfile index ed1a031..fb72b03 100644 --- a/justfile +++ b/justfile @@ -47,11 +47,17 @@ build mode="live": # Deploy all three sites to Cloudflare # On `live`, any post that wasn't already on moq.dev gets mailed to subscribers. deploy env="staging": (build env) + # The player sites go first. They're independent of the blog, and the + # snapshot/deploy/announce sequence below is a transaction: a failure between + # the moq.dev upload and the announcement leaves posts live but unannounced, + # and the snapshot expires after an hour, so a later retry sees the new posts + # as already-published and stays silent forever. + bun wrangler deploy --config sites/pub/wrangler.jsonc --env {{env}} + bun wrangler deploy --config sites/watch/wrangler.jsonc --env {{env}} + # Record what's live before we replace it, so we can tell what the deploy added. bun scripts/notify-subscribers.ts snapshot --env {{env}} bun wrangler deploy --env {{env}} - bun wrangler deploy --config sites/pub/wrangler.jsonc --env {{env}} - bun wrangler deploy --config sites/watch/wrangler.jsonc --env {{env}} just _announce {{env}} # Mail subscribers about anything this deploy published. diff --git a/sites/lib/broadcast.ts b/sites/lib/broadcast.ts index e786f49..7928f89 100644 --- a/sites/lib/broadcast.ts +++ b/sites/lib/broadcast.ts @@ -35,6 +35,31 @@ export function path({ project, name }: Broadcast): string { return `/${parts.map(encodeURIComponent).join("/")}`; } +/** + * The moq.watch URL that plays a broadcast back. + * + * `relay` carries over, because a viewer pointed at the default relay finds + * nothing when the broadcast went somewhere else. + * + * `jwt` deliberately does *not*. moq tokens are prefix-scoped with separate + * publish (`put`) and subscribe (`get`) grants, and a token being used to + * publish necessarily carries `put` — copying it into a link meant to be handed + * around would give every recipient the right to publish, not just to watch. A + * publisher who wants to share access to a private broadcast passes a + * viewer-scoped token as `?viewer=`, which becomes the `?jwt=` on the other end. + */ +export function watch(broadcast: Broadcast, params: URLSearchParams, base: string): URL { + const url = new URL(path(broadcast), base); + + const relay = params.get("relay"); + if (relay) url.searchParams.set("relay", relay); + + const viewer = params.get("viewer"); + if (viewer) url.searchParams.set("jwt", viewer); + + return url; +} + /** * The relay URL to connect to: `/`, carrying `?jwt=` when the * page was given a token. diff --git a/sites/pub/index.html b/sites/pub/index.html index a253df5..f453c5c 100644 --- a/sites/pub/index.html +++ b/sites/pub/index.html @@ -43,12 +43,21 @@ text-decoration: underline; } + .note { + margin: 0; + padding: 0 1rem; + color: #94a3b8; + font-size: 0.8rem; + text-align: center; + } + moq-publish-ui { display: block; width: 100%; max-width: 1280px; aspect-ratio: 16 / 9; - max-height: calc(100vh - 3rem); + /* Leave room for the share link, and the note that can sit under it. */ + max-height: calc(100vh - 6rem); } moq-publish { diff --git a/sites/pub/src/main.ts b/sites/pub/src/main.ts index 6a06c2d..c6eec67 100644 --- a/sites/pub/src/main.ts +++ b/sites/pub/src/main.ts @@ -6,6 +6,8 @@ // Everything that isn't part of the broadcast's identity stays in the query: // ?relay= Relay server URL (default: the relay this site was built for) // ?jwt= Appended to the relay URL as ?jwt= +// ?viewer= A subscribe-only token to put in the moq.watch link. Kept +// separate from ?jwt= on purpose: see Broadcast.watch. // ?source= Preselect a capture source: camera, screen, or file import "@moq/publish/element"; import "@moq/publish/ui"; @@ -33,9 +35,21 @@ function mount(broadcast: Broadcast.Broadcast) { share.className = "share"; share.target = "_blank"; share.rel = "noreferrer"; - share.href = new URL(Broadcast.path(broadcast), WATCH_URL).toString(); + share.href = Broadcast.watch(broadcast, params, WATCH_URL).toString(); share.textContent = share.href.replace(/^https?:\/\//, ""); + document.body.appendChild(share); + + // Publishing against a token but with no viewer token to hand out: the link + // above reaches the right relay but won't authenticate, so say so rather than + // let it look like a working share link. + if (params.get("jwt") && !params.get("viewer")) { + const note = document.createElement("p"); + note.className = "note"; + note.textContent = "Viewers need their own token — pass one as ?viewer= to include it here."; + document.body.appendChild(note); + } + const publish = document.createElement("moq-publish"); publish.setAttribute("url", relay.toString()); publish.setAttribute("name", broadcast.name); @@ -54,5 +68,5 @@ function mount(broadcast: Broadcast.Broadcast) { const ui = document.createElement("moq-publish-ui"); ui.appendChild(publish); - document.body.append(share, ui); + document.body.appendChild(ui); } From ea510cdc1cb08a28aee4af68447ddca9be4c5585 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 5 Aug 2026 09:15:30 -0700 Subject: [PATCH 3/4] Don't let a malformed URL take the Worker down `new URL` keeps a bad percent escape in the pathname, so a request to moq.pub/anon/% reached decodeURIComponent in parse() and threw a URIError straight out of the fetch handler. Verified against wrangler dev: 500 before, 200 after. It's now treated as "not a broadcast", which lands on the invented-name redirect or the usage hint. relay() had the same shape one function down: `?relay=` is whatever was in the address bar and `new URL` throws on garbage, blanking the page. It now falls back to the site's own relay. It also only accepts http and https -- `javascript:` and `data:` parse fine as URLs, mean nothing to WebTransport, and are the shape that becomes an XSS sink as soon as something renders the value. Also correct CLAUDE.md: the dev plugin serves `just dev-pub` and `just dev-watch`, not `bun run dev`, which is the Astro site. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 3 ++- sites/lib/broadcast.ts | 48 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8773288..8c18d9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,7 +86,8 @@ and the Vite dev server so they can't drift: - `route.ts` — which requests redirect: a bare `moq.pub/` invents a random name, and old `?project=&name=` links move into the path - `worker.ts` — the Worker both sites export -- `dev.ts` — a Vite plugin giving `bun run dev` the same routing +- `dev.ts` — a Vite plugin giving `just dev-pub` / `just dev-watch` the same + routing (`just dev` is the Astro site and doesn't use it) Two things are easy to break here: diff --git a/sites/lib/broadcast.ts b/sites/lib/broadcast.ts index 7928f89..733809c 100644 --- a/sites/lib/broadcast.ts +++ b/sites/lib/broadcast.ts @@ -17,13 +17,20 @@ export interface Broadcast { /** Parse `//`, or undefined when the path isn't a broadcast. */ export function parse(pathname: string): Broadcast | undefined { - const parts = pathname - .split("/") - .filter((part) => part !== "") - .map(decodeURIComponent); + const raw = pathname.split("/").filter((part) => part !== ""); // A project on its own isn't enough to publish or watch anything. - if (parts.length < 2) return undefined; + if (raw.length < 2) return undefined; + + // `new URL` keeps a malformed escape like `/anon/%` in the pathname, so this + // is reachable from any request. Treat it as "not a broadcast" rather than + // letting the URIError out of the Worker as a 500. + let parts: string[]; + try { + parts = raw.map(decodeURIComponent); + } catch { + return undefined; + } const [project, ...name] = parts; return { project, name: name.join("/") }; @@ -65,7 +72,7 @@ export function watch(broadcast: Broadcast, params: URLSearchParams, base: strin * page was given a token. */ export function relay(broadcast: Broadcast, params: URLSearchParams, fallback: string): URL { - const url = new URL(params.get("relay") || fallback); + const url = new URL(override(params.get("relay")) ?? fallback); url.pathname = `${url.pathname.replace(/\/+$/, "")}/${broadcast.project}`; const jwt = params.get("jwt"); @@ -73,3 +80,32 @@ export function relay(broadcast: Broadcast, params: URLSearchParams, fallback: s return url; } + +/** + * A `?relay=` override, or undefined when it isn't a usable relay. + * + * The value is whatever was in the address bar, so two things are checked. + * `new URL` throws outright on garbage, and falling back to the site's own + * relay beats a blank page from an uncaught TypeError. It also *accepts* + * `javascript:` and `data:`, which are meaningless to WebTransport and are the + * shape of URL that turns into an XSS sink the moment something renders it, so + * only http and https get through. + */ +function override(relay: string | null): string | undefined { + if (!relay) return undefined; + + let url: URL; + try { + url = new URL(relay); + } catch { + console.warn(`ignoring ?relay=${relay}: not a valid URL`); + return undefined; + } + + if (url.protocol !== "https:" && url.protocol !== "http:") { + console.warn(`ignoring ?relay=${relay}: expected http or https`); + return undefined; + } + + return url.toString(); +} From 8f5957ba3656dc9de361fecf693b7e8b96941e67 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 5 Aug 2026 09:22:43 -0700 Subject: [PATCH 4/4] Drop the moq.watch link from moq.pub The link was never asked for; I added it as a "nice touch" and it earned its keep in review findings rather than use. To work it had to carry ?relay=, and the obvious next step of carrying the whole query would have copied ?jwt= too, leaking a publish token to everyone the link reached. The ?viewer= parameter existed only to escape that bind: a second, subscribe-only token supplied purely so the link could be built. None of that machinery is needed. The path symmetry is the actual feature -- the same // works on both sites -- and swapping the hostname is something a person can do unaided. Removes the link, the note, ?viewer=, Broadcast.watch(), PUBLIC_WATCH_URL from all three .env files, and the layout reserve they needed. moq.watch's usage hint still points at moq.pub; that's the empty state of a page with nothing else to say, not a link hung over a working player. Co-Authored-By: Claude Opus 5 --- .env.development | 1 - .env.live | 1 - .env.staging | 1 - CLAUDE.md | 20 ++++++++------------ justfile | 3 +-- sites/env.d.ts | 2 -- sites/lib/broadcast.ts | 25 ------------------------- sites/pub/index.html | 27 +-------------------------- sites/pub/src/main.ts | 23 ----------------------- 9 files changed, 10 insertions(+), 93 deletions(-) diff --git a/.env.development b/.env.development index ce17928..fb4f66a 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1 @@ PUBLIC_RELAY_URL="http://localhost:4443" -PUBLIC_WATCH_URL="http://localhost:5173" diff --git a/.env.live b/.env.live index 5476a62..5b0fc22 100644 --- a/.env.live +++ b/.env.live @@ -1,3 +1,2 @@ PUBLIC_RELAY_URL="https://cdn.moq.pro" PUBLIC_CLOUDFLARE_URL="https://relay.cloudflare.mediaoverquic.com" -PUBLIC_WATCH_URL="https://moq.watch" diff --git a/.env.staging b/.env.staging index 5b81e2e..a631747 100644 --- a/.env.staging +++ b/.env.staging @@ -1,3 +1,2 @@ PUBLIC_RELAY_URL="https://cdn.moq.wtf" PUBLIC_CLOUDFLARE_URL="https://relay.cloudflare.mediaoverquic.com" -PUBLIC_WATCH_URL="https://new.moq.watch" diff --git a/CLAUDE.md b/CLAUDE.md index 8c18d9c..fef7768 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,17 +67,14 @@ 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` and -`?viewer=`. - -**`jwt` is never copied into the moq.watch link that moq.pub shows.** moq tokens -are prefix-scoped with separate publish (`put`) and subscribe (`get`) grants, so -a token being used to publish carries `put` — putting it in a link meant to be -passed around would hand every recipient the right to publish. `relay` does -carry over, since a viewer on the wrong relay finds nothing. To share access to -a private broadcast, pass a subscribe-only token as `?viewer=`; it becomes the -`?jwt=` on the moq.watch end. With a `jwt` and no `viewer`, the page says so -rather than showing a link that looks fine and won't connect. +string: `?relay=`, `?jwt=`, and (moq.pub only) `?source=camera`. + +**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 +to carry `?relay=` to reach the right relay, and the moment it carries `?jwt=` +too it leaks a publish token, because moq tokens are prefix-scoped with separate +publish (`put`) and subscribe (`get`) grants. Anyone sharing a broadcast can +swap the hostname themselves. `sites/lib` holds the scheme itself, shared by both sites and by both the Worker and the Vite dev server so they can't drift: @@ -128,4 +125,3 @@ Broadcasts cannot be recalled, so the script refuses to guess: an unreachable or - The `@moq/publish` and `@moq/watch` packages handle all MoQ protocol implementation - For new blog posts, add MDX files to `src/pages/blog/` - Component changes in `src/components/` automatically reload with HMR -- moq.pub links to moq.watch, so `just dev-pub` expects `just dev-watch` on :5173 diff --git a/justfile b/justfile index fb72b03..0213c8b 100644 --- a/justfile +++ b/justfile @@ -87,8 +87,7 @@ dev: # Run the web development server bun astro dev --open -# Run the moq.pub development server on :5174. It links broadcasts to :5173, -# so run `just dev-watch` alongside it to follow those links. +# Run the moq.pub development server on :5174. dev-pub: bun i bun vite sites/pub --open diff --git a/sites/env.d.ts b/sites/env.d.ts index 7852afe..b9e08e1 100644 --- a/sites/env.d.ts +++ b/sites/env.d.ts @@ -3,6 +3,4 @@ interface ImportMetaEnv { /** The relay to connect to, e.g. https://cdn.moq.pro. */ readonly PUBLIC_RELAY_URL: string; - /** Where moq.watch is served, used to link a broadcast to its playback URL. */ - readonly PUBLIC_WATCH_URL: string; } diff --git a/sites/lib/broadcast.ts b/sites/lib/broadcast.ts index 733809c..f615d8b 100644 --- a/sites/lib/broadcast.ts +++ b/sites/lib/broadcast.ts @@ -42,31 +42,6 @@ export function path({ project, name }: Broadcast): string { return `/${parts.map(encodeURIComponent).join("/")}`; } -/** - * The moq.watch URL that plays a broadcast back. - * - * `relay` carries over, because a viewer pointed at the default relay finds - * nothing when the broadcast went somewhere else. - * - * `jwt` deliberately does *not*. moq tokens are prefix-scoped with separate - * publish (`put`) and subscribe (`get`) grants, and a token being used to - * publish necessarily carries `put` — copying it into a link meant to be handed - * around would give every recipient the right to publish, not just to watch. A - * publisher who wants to share access to a private broadcast passes a - * viewer-scoped token as `?viewer=`, which becomes the `?jwt=` on the other end. - */ -export function watch(broadcast: Broadcast, params: URLSearchParams, base: string): URL { - const url = new URL(path(broadcast), base); - - const relay = params.get("relay"); - if (relay) url.searchParams.set("relay", relay); - - const viewer = params.get("viewer"); - if (viewer) url.searchParams.set("jwt", viewer); - - return url; -} - /** * The relay URL to connect to: `/`, carrying `?jwt=` when the * page was given a token. diff --git a/sites/pub/index.html b/sites/pub/index.html index f453c5c..55af9de 100644 --- a/sites/pub/index.html +++ b/sites/pub/index.html @@ -23,32 +23,8 @@ body { display: flex; - flex-direction: column; align-items: center; justify-content: center; - gap: 0.75rem; - } - - /* Where to watch this broadcast back: the same path on moq.watch. */ - .share { - color: #7aa2f7; - font-size: 0.9rem; - text-decoration: none; - overflow-wrap: anywhere; - padding: 0 1rem; - text-align: center; - } - - .share:hover { - text-decoration: underline; - } - - .note { - margin: 0; - padding: 0 1rem; - color: #94a3b8; - font-size: 0.8rem; - text-align: center; } moq-publish-ui { @@ -56,8 +32,7 @@ width: 100%; max-width: 1280px; aspect-ratio: 16 / 9; - /* Leave room for the share link, and the note that can sit under it. */ - max-height: calc(100vh - 6rem); + max-height: 100vh; } moq-publish { diff --git a/sites/pub/src/main.ts b/sites/pub/src/main.ts index c6eec67..18d4509 100644 --- a/sites/pub/src/main.ts +++ b/sites/pub/src/main.ts @@ -6,8 +6,6 @@ // Everything that isn't part of the broadcast's identity stays in the query: // ?relay= Relay server URL (default: the relay this site was built for) // ?jwt= Appended to the relay URL as ?jwt= -// ?viewer= A subscribe-only token to put in the moq.watch link. Kept -// separate from ?jwt= on purpose: see Broadcast.watch. // ?source= Preselect a capture source: camera, screen, or file import "@moq/publish/element"; import "@moq/publish/ui"; @@ -15,7 +13,6 @@ import "@moq/publish/ui"; import * as Broadcast from "../../lib/broadcast"; const DEFAULT_RELAY = import.meta.env.PUBLIC_RELAY_URL ?? "https://cdn.moq.pro"; -const WATCH_URL = import.meta.env.PUBLIC_WATCH_URL ?? "https://moq.watch"; const broadcast = Broadcast.parse(location.pathname); if (broadcast) { @@ -30,26 +27,6 @@ function mount(broadcast: Broadcast.Broadcast) { const params = new URLSearchParams(location.search); const relay = Broadcast.relay(broadcast, params, DEFAULT_RELAY); - // The same path on moq.watch plays this broadcast back. - const share = document.createElement("a"); - share.className = "share"; - share.target = "_blank"; - share.rel = "noreferrer"; - share.href = Broadcast.watch(broadcast, params, WATCH_URL).toString(); - share.textContent = share.href.replace(/^https?:\/\//, ""); - - document.body.appendChild(share); - - // Publishing against a token but with no viewer token to hand out: the link - // above reaches the right relay but won't authenticate, so say so rather than - // let it look like a working share link. - if (params.get("jwt") && !params.get("viewer")) { - const note = document.createElement("p"); - note.className = "note"; - note.textContent = "Viewers need their own token — pass one as ?viewer= to include it here."; - document.body.appendChild(note); - } - const publish = document.createElement("moq-publish"); publish.setAttribute("url", relay.toString()); publish.setAttribute("name", broadcast.name);