← Back to blog

niacin 1.6 — direct downloads now auto-update via Sparkle 2

If you installed niacin from a signed GitHub release, you no longer need to come back here to get the next one. As of 1.6, every direct-download copy polls https://niacin.dort.zone/appcast.xml in the background, verifies the new build against an EdDSA key baked into the app, and offers the update with a familiar Sparkle prompt. Mac App Store builds are unaffected — Apple already handles updates there — and IT-managed fleets get a one-key MDM kill switch that hides the entire feature.

The headline release is 1.6, but the feature arrived in 1.3 and took two follow-up releases to land cleanly. The sections below walk through what shipped, the MDM surface, and the one structural change that turned out to be unavoidable: dropping the App Sandbox.

What “auto-update” actually means here

niacin embeds Sparkle 2 via Swift Package Manager. On launch and on a configurable schedule, Sparkle fetches the appcast, compares sparkle:shortVersionString against the running build, and — if there’s a newer entry — downloads the <enclosure> .zip from GitHub Releases, verifies its EdDSA signature against SUPublicEDKey in Info.plist, and prompts the user to install and relaunch.

The .zip itself is built and notarised by scripts/release.sh in the niacin repo. Each release run prints a ready-to-paste <item> block with the EdDSA signature already filled in — no signing-key handling on this site, ever. The web repo’s appcast.xml is a pure deploy mirror of the file the script generates.

The MDM kill switch

The whole feature is gated behind a single managed preference: disableAutoUpdate under com.oldsalt.niacin. When IT pushes disableAutoUpdate = true via a configuration profile:

  • Sparkle’s automaticallyChecksForUpdates flips to false and any in-flight check is silenced.
  • The “Check for Updates…” menu bar item is hidden.
  • The Settings “Software Update” section’s toggle locks and shows the managed badge, and the Managed Policy panel surfaces “Auto-updates disabled by policy”.

It hooks the same PolicyWatcher funnel that 1.1 introduced for live policy reload, so flipping the key over MDM takes effect within milliseconds — no logout, no relaunch. The default is false: unmanaged users get auto-updates without lifting a finger, managed orgs opt out with one key.

Settings: a new “Software Update” section

Settings gains a small Software Update panel with three controls: an “Automatically check for updates” toggle, a “Check frequency” picker (Daily / Weekly / Monthly — mapped to Sparkle’s update interval in seconds), and a “Check Now…” button that triggers SPUUpdater.checkForUpdates(). The toggle uses the same ManagedToggle component as the existing managed-policy controls, so the locked-by-MDM visual treatment is consistent across the window.

All nine new strings (Software Update, Automatically check for updates, Check frequency, Check Now, Check for Updates…, Auto-updates disabled by policy, Daily, Weekly, Monthly) are translated into French, German, Spanish, and Italian to match the 1.2 localisation baseline.

The unavoidable change: dropping the App Sandbox in 1.5

The honest part of this release: 1.3 shipped Sparkle inside the App Sandbox, and the sandboxed install path failed for the most common deployment — the app sitting in /Applications. Sparkle’s installer needs to acquire admin rights to replace a system-domain bundle, and a sandboxed app can’t. The exact failure was “Failed to copy system domain rights: -60005”, with the update silently giving up after the user clicked Install.

The available workarounds were:

  • Install to ~/Applications. Avoids admin rights but is awkward UX, easy to break with quarantine / app translocation, and not where most Mac users expect apps to live.
  • Build a privileged helper via SMJobBless / SMAppService. Days of work and ongoing complexity for a feature that should be invisible.
  • Drop the App Sandbox.

1.5 took option three. Niacin’s threat model doesn’t lean on the sandbox in the first place — the app reads world-readable plists from /Library/Managed Preferences and spawns /usr/bin/caffeinate, both of which the sandbox couldn’t meaningfully constrain anyway. What does still constrain the binary is Hardened Runtime, Developer ID code signing, and notarisation, all of which stay on. Gatekeeper still trusts every build, and the EdDSA verification on each Sparkle download is an additional layer the sandbox never provided.

The cost is real, though, and worth naming: niacin can no longer be distributed through the Mac App Store, since App Store submissions require the sandbox. The MAS listing for 1.0–1.2 stays valid for users who want it, but the active distribution channel going forward is Developer ID + the appcast (and managed installs via MDM, which never went through MAS anyway).

Verifying it works

If you’re on 1.3 or later and want to confirm: Settings → Software Update → Check Now should reach niacin.dort.zone/appcast.xml and report “You’re up to date” or offer the latest release. Under the hood, every install verifies that downloaded .zip against the public EdDSA key in Info.plist before unpacking — a tampered enclosure on the GitHub Releases CDN would fail the signature check and never run.

For IT teams: pushing com.oldsalt.niacin.disableAutoUpdate = true via a configuration profile should remove the menu bar “Check for Updates…” item within roughly a second, lock the Settings toggle, and add the policy row. Removing the key reverses all three.

Upgrading

Already on 1.3 or later? Sparkle will offer 1.6 the next time it polls the feed (or right now via “Check Now”). On 1.0–1.2 from a GitHub release? Download the 1.6 .zip from the releases page, replace the app, relaunch — from there forward, you’re on the auto-update track. Mac App Store users will get the next compatible MAS update through the App Store as usual; the auto-update plumbing is dormant in those builds.

Source, EdDSA public key, and release notes are on GitHub at github.com/just-an-oldsalt/niacin. Bug reports, especially anything involving an MDM-managed deployment refusing to honour the kill switch, go to the issue tracker or niacin@dort.zone.