# 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/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.

## How it works

- **Routing.** Three path-scoped Worker routes (`/<channel>/latest`) sit on the updates hostname.
  Worker routes take precedence over an R2 custom domain on the same host, so *only* those three
  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**.
- **302, short TTL.** The 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`).

## Develop

```sh
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`):

```sh
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.

```sh
npm run deploy        # wrangler deploy — registers the three /<channel>/latest routes
```

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

```sh
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).
