Enables Bunmaska apps to update themselves from a channel feed that bunmaska build --update produces. It is a drop-in subset of Electron’s autoUpdater, but built on plain Bun rather than Squirrel: it reads an update.json manifest, compares versions, downloads the artifact, verifies its Ed25519 signature plus size and content hash, decompresses (zstd) and stages a .tar, then swaps the installed bundle and relaunches via a detached helper.
Process: Main. The autoUpdater singleton is a Node.js EventEmitter. Two things that differ from Electron up front, so you don’t get surprised:
- The flow is electron-updater style, not Electron-core style.
checkForUpdates()does not download automatically - you calldownloadUpdate()yourself once an update is available. - Updates must be signed.
downloadUpdate()refuses to install anything without a valid detached.sigmatching thepublicKeyyou pass tosetFeedURL. Generate the key pair withbunmaska keygen, sign releases withbunmaska build --update --update-key, and bake the public key into your app. An integrity hash alone is not trust - a compromised feed controls the manifest, so only the signature proves the bytes came from you.
import { autoUpdater } from 'bunmaska';
autoUpdater.setFeedURL({
url: 'https://updates.example.com/myapp/stable',
publicKey: UPDATE_PUBLIC_KEY_PEM, // from `bunmaska keygen`
});
autoUpdater.on('update-available', () => autoUpdater.downloadUpdate());
autoUpdater.on('update-downloaded', () => autoUpdater.quitAndInstall());
autoUpdater.on('error', (err) => console.error('update failed', err));
await autoUpdater.checkForUpdates();
The end-to-end publishing flow (keygen, build flags, hosting the feed) is walked through in Building & Distribution.
Methods
autoUpdater.setFeedURL(options)
setFeedURL(options: { url: string; publicKey?: string; channel?: string } | string): void
Sets the base URL of the channel feed - the directory where update.json, the artifact, and its .sig live. Accepts either an options object or a bare string. Throws if the URL is missing, empty, or unparseable.
The URL must be https; a plaintext feed is refused at set time (http is allowed only for localhost/127.0.0.1/[::1], so you can test against a local server).
publicKey- the PEM Ed25519 public key every downloaded artifact’s.sigmust verify against. This is your release key frombunmaska keygen, baked into your app - not a Bunmaska key. Without it,downloadUpdate()refuses to download anything.channel- if set, a manifest whosechanneldiffers is rejected (guards against channel confusion, e.g. a canary build being offered to stable users).
Electron’s headers, serverType, and allowAnyVersion options are not implemented (the feed is a static directory + JSON, so there’s nothing to authenticate or negotiate).
import { autoUpdater } from 'bunmaska';
// object form - what production apps should use
autoUpdater.setFeedURL({
url: 'https://updates.example.com/myapp/stable',
publicKey: UPDATE_PUBLIC_KEY_PEM,
channel: 'stable',
});
// string form is accepted, but leaves no publicKey - fine for checking,
// useless for downloading
autoUpdater.setFeedURL('https://updates.example.com/myapp/canary');
autoUpdater.getFeedURL()
getFeedURL(): string
Returns the configured feed URL, or '' if setFeedURL has not been called yet.
import { autoUpdater } from 'bunmaska';
autoUpdater.setFeedURL({ url: 'https://updates.example.com/myapp/stable' });
console.log(autoUpdater.getFeedURL()); // "https://updates.example.com/myapp/stable"
autoUpdater.checkForUpdates()
checkForUpdates(): Promise<UpdateCheckResult | null>
Fetches <feedURL>/update.json and compares its version against the running app’s version. Emits checking-for-update, then either update-available or update-not-available.
Resolves to { updateInfo, manifest } when a newer version exists, or null when the app is up to date. Rejects (and emits error) on a network or manifest-parse failure, when the manifest targets a different OS/architecture than the running build, or when its channel differs from the one you configured. You must call setFeedURL first, or it throws.
Unlike Electron, this does not trigger a download - it only tells you whether one is available.
import { autoUpdater } from 'bunmaska';
autoUpdater.setFeedURL({ url: 'https://updates.example.com/myapp/stable' });
const result = await autoUpdater.checkForUpdates();
if (result) {
console.log(`update ${result.updateInfo.version} available`);
} else {
console.log('already on the latest version');
}
autoUpdater.downloadUpdate()
downloadUpdate(): Promise<StagedUpdate>
Downloads the artifact for the update found by the most recent checkForUpdates() and runs the full verification chain:
- Size caps - the declared compressed size must be within 512 MB before any bytes are fetched, and the decompressed tar within 2 GB (a zip-bomb guard).
- Byte length + wyhash - the downloaded bytes must match the manifest’s
sizeandhash. - Ed25519 signature -
<artifact>.sigis fetched from the feed and verified against thepublicKeyfromsetFeedURL. No key configured, or a bad signature, and the whole download rejects - unsigned updates are refused.
Only then is the artifact decompressed (zstd) and the resulting .tar staged on disk. Emits update-downloaded on success. Rejects (and emits error) if no update is pending - call checkForUpdates() first - or if any step above fails. Resolves to a StagedUpdate ({ manifest, tarPath }).
This explicit step has no equivalent in Electron’s core autoUpdater (where download is implicit); it mirrors electron-updater.
import { autoUpdater } from 'bunmaska';
autoUpdater.on('update-available', async () => {
const staged = await autoUpdater.downloadUpdate();
console.log(`staged ${staged.manifest.version} at ${staged.tarPath}`);
});
autoUpdater.quitAndInstall()
quitAndInstall(): void
Installs the staged update and relaunches. Throws if nothing has been downloaded - call downloadUpdate() first. Should only be called after update-downloaded has been emitted.
The default installer is a real swap, not a stub: it writes a small detached helper (a /bin/sh script on macOS/Linux, a cmd script on Windows), quits the app, and the helper then waits for the process to exit, extracts the staged tar into a temp sibling of the install root, rename-swaps it into place (so a half-finished extract can never brick the installed app - the second rename rolls back the first on failure), relaunches the new build, and deletes itself.
Two honest caveats:
- It refuses to swap when the running process is not an installed bundle (e.g.
bun main.tsin dev, or a bare binary outside the.app/AppDir/portable-dir layoutbunmaska buildproduces) - it logs a warning and just quits, leaving the staged tar in place. - The helper-script generators are unit-tested on all three platforms; the live swap itself is the one step the test suite does not exercise end to end. If you need a different install strategy, the installer is injectable (see Replacing the installer below).
import { autoUpdater } from 'bunmaska';
autoUpdater.on('update-downloaded', () => {
// quits the app; a detached helper swaps the bundle and relaunches
autoUpdater.quitAndInstall();
});
Events
The autoUpdater object emits the following events.
Event: ‘checking-for-update’
Emitted when checkForUpdates() begins. No arguments.
import { autoUpdater } from 'bunmaska';
autoUpdater.on('checking-for-update', () => console.log('checking…'));
Event: ‘update-available’
Returns:
updateInfoUpdateInfo -{ version, releaseName }.
Emitted when a newer version is found. The update is not downloaded automatically - call downloadUpdate() in this handler if you want it.
import { autoUpdater } from 'bunmaska';
autoUpdater.on('update-available', (info) => {
console.log(`v${info.version} (${info.releaseName}) is available`);
autoUpdater.downloadUpdate();
});
Event: ‘update-not-available’
Returns:
updateInfoUpdateInfo - the manifest’s{ version, releaseName }, even though it isn’t newer.
Emitted when the feed’s version is not newer than the running app.
import { autoUpdater } from 'bunmaska';
autoUpdater.on('update-not-available', () => console.log('up to date'));
Event: ‘update-downloaded’
Returns:
updateInfoUpdateInfo -{ version, releaseName }.
Emitted after downloadUpdate() has fetched, verified (size, hash, signature), and staged the update. Call quitAndInstall() to apply it.
Note the payload differs from Electron: Bunmaska emits a single UpdateInfo object. Electron’s releaseNotes, releaseDate, and updateURL fields are not present, because the update.json manifest does not carry them.
import { autoUpdater } from 'bunmaska';
autoUpdater.on('update-downloaded', (info) => {
console.log(`v${info.version} ready to install`);
});
Event: ‘error’
Returns:
errorError
Emitted when a check, download, verification, or install fails. As in Electron, attach a listener - an unhandled error event on an EventEmitter will throw. (Internally, the updater only emits error when a listener is attached, but the promise still rejects either way, so always handle one or the other.)
import { autoUpdater } from 'bunmaska';
autoUpdater.on('error', (err) => console.error('auto-update error:', err));
Types
For reference, the small object shapes used above:
type UpdateInfo = { version: string; releaseName: string };
type StagedUpdate = {
manifest: UpdateManifest; // parsed update.json
tarPath: string; // decompressed .tar staged on disk
};
type UpdateCheckResult = { updateInfo: UpdateInfo; manifest: UpdateManifest };
Replacing the installer
Every side effect (network fetch, decompress, disk staging, install) is an injectable dependency, which is how the check/download/verify engine stays unit-testable. The most useful seam for apps with special install requirements is install: supply your own to replace the default detached swap-and-relaunch.
import { AutoUpdaterImpl } from 'bunmaska';
const updater = new AutoUpdaterImpl({
install: (staged) => {
// staged.tarPath is the verified, decompressed bundle
myInstaller.applyAndRelaunch(staged.tarPath);
},
});
setDepsForTesting() exists too, but as the name says, it is for tests - don’t reach for it in app code.
Not in Bunmaska (yet)
Compared to Electron’s autoUpdater, the following are intentionally absent:
- Squirrel.Windows / MSIX - Bunmaska runs the same plain tar + zstd pipeline on Windows as elsewhere, so there is no Squirrel.Windows, no MSIX detection, no
allowAnyVersiondowngrade option, and no--squirrel-firstrunhandling. - Squirrel.Mac - even on macOS there is no Squirrel. Trust comes from the Ed25519 signature on the artifact; code-signing the swapped bundle for Gatekeeper is still on you (sign what you feed to
bunmaska build --update). - Event:
before-quit-for-update- not emitted.quitAndInstall()goes straight through the installer seam (the default callsapp.quit()after spawning the helper). - Automatic download - Electron downloads as soon as an update is available; Bunmaska makes it an explicit
downloadUpdate()call (electron-updater style). This is a deliberate behavioral difference, not a missing feature. setFeedURLoptionsheaders,serverType,allowAnyVersion- the accepted options areurl,publicKey, andchannel. The feed is a static directory, so there’s nothing to send headers to or negotiate a server type with.- Rich
update-downloadedpayload -releaseNotes,releaseNameas a standalone arg,releaseDate, andupdateURLare not provided. You get a singleUpdateInfo(version,releaseName); theupdate.jsonmanifest carries nothing more.