Files
nucleic/cloud/nucleic-updates

nucleic-updates

A tiny redirect Worker layered on the Sparkle update host. See docs/CLOUD_INFRA.md §1 for the full update-hosting design.

updates.nucleic.blakeslee.xyz is served directly by the nucleic-updates R2 bucket (custom domain, no Worker in the path — fully CDN-cacheable). This Worker exists for one job: resolve a channel's latest alias to the current versioned DMG, so anything elsewhere — the website's "Download" button, docs, a chat link — can point at one stable URL that never needs updating:

GET https://updates.nucleic.blakeslee.xyz/canary/latest → 302 → …/canary/Nucleic-Canary-<ver>.dmg
GET https://updates.nucleic.blakeslee.xyz/beta/latest   → 302 → …/beta/Nucleic-Beta-<ver>.dmg
GET https://updates.nucleic.blakeslee.xyz/rc/latest     → 302 → …/rc/Nucleic-RC-<ver>.dmg
GET https://updates.nucleic.blakeslee.xyz/stable/latest → 302 → …/stable/Nucleic-<ver>.dmg

DMGs are foldered by channel (<channel>/<file>.dmg) for tidy, predictable download links; the appcast feeds stay at the bucket root (appcast-<channel>.xml), where each build's baked-in SUFeedURL points them.

The Worker also fronts the iOS companion (NucleicRemote), which ships via TestFlight rather than Sparkle/R2. Two more aliases are static redirects to the fixed TestFlight invite links:

GET https://updates.nucleic.blakeslee.xyz/testflight/canary → 302 → https://testflight.apple.com/join/nFnHdmAU
GET https://updates.nucleic.blakeslee.xyz/testflight/beta   → 302 → https://testflight.apple.com/join/AaBZpbkp

How it works

  • Routing. Path-scoped Worker routes (/<channel>/latest and /testflight/<channel>) sit on the updates hostname. Worker routes take precedence over an R2 custom domain on the same host, so only those paths reach the Worker; the root appcast-*.xml and the <channel>/*.dmg objects keep being served straight from R2.
  • Latest resolution. The Worker reads the channel's appcast (appcast-<channel>.xml) from the bound R2 bucket and redirects to the enclosure with the highest sparkle:version (the monotonic build number release-macos.sh bumps every release). It's the same XML the in-app updater reads, so the alias and the updater always agree — and there is no per-release config to update.
  • TestFlight aliases. The two /testflight/<channel> paths are static redirects to fixed TestFlight invite links (TESTFLIGHT in src/index.ts) — no appcast, no R2 read. Edit the map and redeploy if a link is ever rotated.
  • 302, short TTL. The latest redirect is a temporary 302 (the target changes each release) cached for 5 min, matching the appcast's own max-age (scripts/upload-r2.sh). The TestFlight redirects are also 302, cached an hour since they only change on a manual link rotation.

Develop

npm install
npm run typecheck      # tsc --noEmit
npm test               # node:test — unit tests for the appcast parser
npm run check          # wrangler deploy --dry-run (bundles, no upload, no auth)
npm run dev            # local server at http://localhost:8787

Local smoke test against npm run dev (needs a real appcast in the bound bucket, or use --remote):

curl -sI http://localhost:8787/beta/latest        # → 302, Location: …/Nucleic-Beta-<ver>.dmg

Deploy (one-time + on demand)

Prereqs: the blakeslee.xyz zone and the nucleic-updates R2 bucket (with its updates.nucleic.blakeslee.xyz custom domain) already exist on this account — both are set up for Sparkle hosting in docs/CLOUD_INFRA.md §1.

npm run deploy        # wrangler deploy — registers the /<channel>/latest + /testflight/<channel> routes

After deploy, verify (a published beta release must exist in the bucket):

curl -sI https://updates.nucleic.blakeslee.xyz/beta/latest
# HTTP/2 302 ; location: https://updates.nucleic.blakeslee.xyz/beta/Nucleic-Beta-<ver>.dmg
curl -sIL https://updates.nucleic.blakeslee.xyz/beta/latest | tail -1   # follows → 200 DMG

No secrets, no KV, no cron — the only binding is the UPDATES R2 bucket (read-only use).