---
title: "Hoxi Desktop updates"
canonical_url: "https://ainode.run/docs/hoxi-desktop-updates"
type: "doc"
locale: "en"
description: "Publish verified Hoxi Desktop releases and consume the canonical Sparkle appcast."
---

# Hoxi Desktop updates
> Publish verified Hoxi Desktop releases and consume the canonical Sparkle appcast.

# Hoxi Desktop Update API

The website implements authenticated release publication and a public Sparkle feed for macOS arm64. The exact canonical feed URL is **[https://hoxi.ai/api/hoxi/appcast.xml](https://hoxi.ai/api/hoxi/appcast.xml)**. Alternate paths are not valid `sparkle.feedURL` values.

## Authentication and Key Pinning

`POST https://hoxi.ai/api/hoxi/releases/publish` accepts `Authorization: Bearer ` or `x-release-key: `. A nonempty Authorization value takes precedence. Use the existing `HOXI_RELEASE_KEY` from the deployment's `.env` through secure secret injection; do not replace it, print it, commit it, or put it in request URLs. Missing server configuration returns HTTP 500; a missing or incorrect supplied key returns HTTP 401. Dry runs require authentication too.

The server's `HOXI_SPARKLE_PUBLIC_KEY` is a pinned Ed25519 public key, encoded as canonical base64 for exactly 32 bytes. It must match both the key embedded in the desktop app and the public key used by release CI. The request cannot choose a different trust anchor. Never put the private signing key on the website server. Do not rotate the public key without a client migration: already installed clients trust their embedded key, and changing the server pin also filters out releases signed with the old key.

## Publish Request

Send JSON with these fields. Signed publication requires the entire `sparkle` object plus top-level `size`, `sha256`, and `minimumMacOS`.

| Field | Contract |
| --- | --- |
| `version` | Required complete `major.minor.patch`, without leading zeroes. Whitespace and an initial `v` are normalized away. |
| `title`, `notes` | Optional release title and notes; omitted/empty values use server defaults. |
| `size` | Positive safe integer byte count, at most 536870912 bytes (512 MiB); must equal `sparkle.length`. |
| `sha256` | Exactly 64 lowercase hexadecimal characters; hash of the downloadable DMG bytes. |
| `minimumMacOS` | String in `major.minor` or `major.minor.patch` form, emitted as Sparkle's minimum system version. |
| `sparkle.version` | Exact normalized top-level version, with no `v` prefix. |
| `sparkle.url` | Exactly `https://github.com/hoxiai/hoxi/releases/download/v{version}/Hoxi-{version}-darwin-arm64.dmg`. |
| `sparkle.feedURL` | Exactly `https://hoxi.ai/api/hoxi/appcast.xml`. |
| `sparkle.publicKey` | Canonical base64 for 32 bytes, identical to the server pin. |
| `sparkle.edSignature` | Canonical base64 for a 64-byte Ed25519 signature over the DMG bytes. |
| `sparkle.length` | Numeric byte count identical to `size`. |
| `mandatory` | Optional boolean, defaults to `false`; stored as release metadata, not an appcast enforcement flag. |
| `desktop` | Optional platform-to-download-URL map for existing download consumers. |
| `mirrors` | Optional platform-to-`{ cn?, global? }` URL map. Neither map changes the signed Sparkle URL. |
| `dryRun` | Optional boolean, defaults to `false`. |

The macOS arm64 map key is `mac-arm64`. Publication uses the normalized version's stable slug, `hoxi-desktop-v{version}`, to create or update a changelog record rather than intentionally creating a second record for a retry.

## Dry Run and Verified Publication

With `dryRun: true`, the handler authenticates, parses the request, normalizes the version, and validates supplied Sparkle metadata against the configured pin. It performs **no database access and no artifact network request**. The response has `code: 0`, `dryRun: true`, and `data` containing `slug`, `version`, `versionCode`, `title`, `notes`, `sha256`, `size`, `mandatory`, and **`sparkleAccepted: false`**. This is a metadata rehearsal, not evidence of a valid signature, an available artifact, a successful write, or feed acceptance. It cannot detect an existing signed-record overwrite conflict.

For a real signed publication, the server performs these steps:

1. Validate all signed metadata and fetch the exact versioned GitHub DMG. Only bounded HTTPS redirects to `release-assets.githubusercontent.com` or `objects.githubusercontent.com` are allowed, with at most five redirects and no forwarded credentials.
2. Require HTTP 200, matching `Content-Length` when present, and an exact actual byte count. Verify SHA-256 and the Ed25519 signature over those bytes using the pinned key.
3. Only after verification succeeds, access the database and upsert the release. Store `sparkle`, `minimumMacOS`, and the `mac-arm64` entries in the `size` and `sha256` maps in release metadata.
4. Read back through the same fresh query and key filter used by the public feed. Require matching persisted Sparkle metadata, SHA-256, and minimum macOS before acknowledging acceptance.

A successful response has `code: 0`, `ok: true`, `success: true`, and `data` with `id`, `slug`, `version`, `versionCode`, `isNew`, and `metaData`. Only a signed publication that passes readback additionally returns **`data.sparkleAccepted: true` and `data.sparkle`**, the persisted Sparkle metadata echo. A generic success or write acknowledgment alone is not signed-feed acceptance.

Omitting `sparkle` retains the legacy publication path; it does not qualify a release for the signed feed. A legacy republish **cannot overwrite an existing signed release**. Republishing a signed version requires complete metadata and fresh artifact verification, not just a title/notes update without `sparkle`.

## Public Feed

`GET https://hoxi.ai/api/hoxi/appcast.xml` requires no publish key and returns `application/xml; charset=utf-8` with `Cache-Control: no-store, no-cache, must-revalidate`. Keep that policy through proxies and CDNs; do not serve a stale cached appcast.

Each request freshly queries up to 50 active `changelog` records with the `hoxi-desktop-` slug prefix, ordered by descending version code and record ID. Only records whose stored signed metadata validates against the current pin are included. Legacy/invalid records are omitted. A missing or invalid server pin returns HTTP 503 rather than an untrusted feed. Feed rendering validates stored metadata; it does not download and reverify every artifact on each GET.

Items include title, escaped release notes, publication date, Sparkle version/short version, `sparkle:minimumSystemVersion`, and an enclosure with the exact signed URL, length, and `sparkle:edSignature`.

## Resource and Deployment Limits

Artifact verification is **buffered in memory**, not constant-memory streaming: chunks are retained and then copied into a contiguous buffer. Plan for approximately **2x artifact size plus cryptographic/runtime overhead per concurrent publication**. At the 512 MiB limit, this means roughly 1 GiB before that overhead; a 512 MiB worker memory budget is insufficient. Account for concurrency and verify the deployment supports Ed25519 Web Crypto.

The fetch/verification operation has a **60-second bound**, including redirects and body reading. Configure the deployment, reverse proxy, and caller request timeout to **more than 60 seconds**, with additional headroom for crypto, database work, and readback. A longer platform timeout does not remove the application's verification bound.

## Failures, Retries, and Rollback

- Authentication, metadata, download, size, hash, signature, or verification-timeout failures before verification completes cause no release database writes. Signed artifact verification occurs before even the existing-record database lookup.
- A database/write error, lost response, or post-write readback failure is different: the release **may already be committed**. A readback mismatch returns HTTP 503 and does not automatically undo the write.
- Inspect the persisted state and use an idempotent retry with the same normalized version and identical complete signed payload. Do not invent another version to recover from a lost acknowledgment. Concurrent publication conflicts may require a retry; do not assume a failed response means no record exists.
- Rollback must be explicit and reviewed. Stop further publication, inspect the release/feed, and correct or deactivate a bad record through authorized operations as appropriate. **Do not automatically delete a release or its artifact** on failure: clients may already have observed it. Preserve valid signed metadata and the key trust chain; any key change requires a client migration.

This documents the implemented website contract in `app/themes/hoxi/api/releases/publish.post.ts`, `api/appcast.xml.get.ts`, `server/shared/releases.ts`, `server/shared/sparkle.ts`, and `utils/hoxi-sparkle.ts`. It does not authorize a production publish, deployment, secret change, or rollback.
