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. Alternate paths are not valid sparkle.feedURL values.

Authentication and Key Pinning

POST https://hoxi.ai/api/hoxi/releases/publish accepts Authorization: Bearer <key> or x-release-key: <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.

FieldContract
versionRequired complete major.minor.patch, without leading zeroes. Whitespace and an initial v are normalized away.
title, notesOptional release title and notes; omitted/empty values use server defaults.
sizePositive safe integer byte count, at most 536870912 bytes (512 MiB); must equal sparkle.length.
sha256Exactly 64 lowercase hexadecimal characters; hash of the downloadable DMG bytes.
minimumMacOSString in major.minor or major.minor.patch form, emitted as Sparkle's minimum system version.
sparkle.versionExact normalized top-level version, with no v prefix.
sparkle.urlExactly https://github.com/hoxiai/hoxi/releases/download/v{version}/Hoxi-{version}-darwin-arm64.dmg.
sparkle.feedURLExactly https://hoxi.ai/api/hoxi/appcast.xml.
sparkle.publicKeyCanonical base64 for 32 bytes, identical to the server pin.
sparkle.edSignatureCanonical base64 for a 64-byte Ed25519 signature over the DMG bytes.
sparkle.lengthNumeric byte count identical to size.
mandatoryOptional boolean, defaults to false; stored as release metadata, not an appcast enforcement flag.
desktopOptional platform-to-download-URL map for existing download consumers.
mirrorsOptional platform-to-{ cn?, global? } URL map. Neither map changes the signed Sparkle URL.
dryRunOptional 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.