Эта страница ещё не переведена. Читать оригинал на английском

API

nsh/updater

nsh/updater checks for a newer version of a packaged app, downloads it and replaces the app. Where updates come from is set in nsh.json’s "updater", never by the code that calls the module, and every step is checked in Rust: the manifest’s signature, the app id, a newer version, the archive’s size and SHA-256. The UI is yours: the module returns state and does the work. App Updates walks through the whole flow, from setting the source to publishing a release.

It also updates the app’s code alone: a signed package of a newer build, which the app runs from its next start on every native platform, iOS and Android included (OTA updates).

import { checkForUpdate, downloadUpdate, installAndRestart } from "nsh/updater";
import { checkForPackage, downloadPackage, restartToPackage, runningPackage, shellInfo } from "nsh/updater";

The module can be imported on every platform:

Platform checkForUpdate downloadUpdate, installAndRestart Packages
macOS, Windows, Linux (packaged) Checks the source Download and replace the app Check, download, restartToPackage restarts into it
iOS, Android Checks the source; url is the store link (nsh update-manifest --link) Reject: send the user to url Check and download; the package runs from the next start
Web, development runs, apps without "updater" Resolves null Reject checkForPackage resolves null, runningPackage() is null, shellInfo() has no version

It needs no capability: capabilities.network does not apply, since the app’s code cannot choose where these requests go. Its requests use the platform’s own network stack, as fetch does.

#checkForUpdate

function checkForUpdate(): Promise<AppUpdate | null>;

interface AppUpdate {
  version: string;
  notes?: string | null;
  url: string;
  canInstall: boolean;
}

Fetches the manifest from the source in nsh.json and resolves the newer version it describes, or null when the app is up to date, has no "updater", or runs where apps do not update (the web, nsh run). A run on a package of that version or a later one is up to date too: the app’s own version stays the one it shipped as, but its code is already the release’s.

Field Meaning
version The new version, semver
notes What changed, as the manifest’s --notes file says it
url Where the user can get it by hand: the archive, or the store page on iOS and Android. Pass it to openUrl when canInstall is false
canInstall Whether downloadUpdate and installAndRestart work in this run: a packaged desktop app installed where the user can write. False on iOS and Android, and under a directory only the system writes (a system package)

It rejects when the source cannot be reached or its manifest is not acceptable: a bad or missing signature when publicKey is set, another app’s id, a URL that is not HTTPS. A manifest of the same or an older version is not an update, so a stale manifest cannot downgrade the app.

#downloadUpdate

function downloadUpdate(onProgress?: ((progress: UpdateProgress) => void) | null): Promise<void>;

interface UpdateProgress {
  received: number;
  total?: number | null;
}

Downloads the archive of the update the last checkForUpdate found into the app’s data directory, calling onProgress as bytes arrive (total is missing when the server does not say), then checks its size and SHA-256 against the manifest. It rejects with ERR_FEATURE_UNAVAILABLE_ON_PLATFORM when no update was found or canInstall is false, and when the download fails or does not match.

const progress = signal(0);
await downloadUpdate(({ received, total }) => progress.set(total ? received / total : 0));

#installAndRestart

function installAndRestart(): Promise<void>;

Unpacks the downloaded archive next to the app, checks the new app (its id and executable, and on macOS its code signature), swaps it in and restarts the app as Application.restart() does: this run quits as when the user quits it, and the new version starts once it has exited. The promise settles only on failure, which leaves the installed app as it was: it rejects when nothing was downloaded or the app cannot be replaced.

On Linux and macOS the whole directory or .app is replaced, keeping the name the user gave it; on Windows the files are replaced one by one and put back if one fails.

#OTA updates

OTA (over-the-air) updates deliver a packaged app’s TSX as a signed package (nsh publish) instead of a new app. OTA Updates walks through the setup, the release and what happens on the device. An app whose nsh.json trusts package roots ("updater": { "packages": { "roots": [...] } }, below) finds it in the same manifest, downloads it, and runs it from its next start. A change to the NSH runtime itself still needs a new app build: each package names the lowest runtime it runs on, and older apps are not offered it.

#checkForPackage

function checkForPackage(): Promise<PackageUpdate | null>;

interface PackageUpdate {
  version: string;
  build: number;
  notes?: string | null;
  size?: number | null;
}

Fetches the manifest as checkForUpdate does and resolves the package it offers when its build is newer than the running one (the running package’s, else the build the app shipped with) and this runtime runs it. It resolves null when there is none, the app trusts no package roots, or it runs where apps do not update (the web, nsh run). It rejects as checkForUpdate does.

#downloadPackage

function downloadPackage(onProgress?: ((progress: UpdateProgress) => void) | null): Promise<void>;

Downloads the package the last checkForPackage found, checks its size and SHA-256 against the manifest, and verifies it as the app’s next start will: signed by a release key one of the roots certifies, for this app, newer than the running build and than every build the app ran before, for this runtime. It then waits in the app’s data directory for the next start. It rejects with ERR_FEATURE_UNAVAILABLE_ON_PLATFORM when the check found none, and with an error saying why (package refused: …) when the download fails or the package is refused, which leaves nothing behind.

#restartToPackage

function restartToPackage(): Promise<void>;

On macOS, Windows and Linux, restarts the app into the downloaded package as Application.restart() does; the promise settles only on failure. It rejects when nothing was downloaded, and on iOS and Android with ERR_FEATURE_UNAVAILABLE_ON_PLATFORM: there the package runs from the app’s next start.

#runningPackage

function runningPackage(): RunningPackage | null;

interface RunningPackage {
  version: string;
  build: number;
}

The package this run’s code came from, or null when the app runs the code it shipped with.

#shellInfo

function shellInfo(): ShellInfo;

interface ShellInfo {
  version: string | null;
  build: number | null;
  nshVersion: string;
  packages: boolean;
}

The installed app this code runs in, its shell: the version and build it was packaged with (nsh.json’s; on iOS its CFBundleShortVersionString and CFBundleVersion), the version of the NSH runtime built into it, and whether it runs signed packages. A package replaces the app’s code, not its shell: runningPackage() says which code runs, shellInfo() what runs it, so a package that needs a newer runtime (minRuntime) tells the user to install the whole app. version and build are null for an app without "updater" in nsh.json, and on the web and in development runs, where packages is false.

#At the next start

The app runs the newest package that verifies and is newer than the build it shipped with. The first run of a new package is a trial: once it has run 10 seconds past its first frame, or the user quits it, it becomes the installed package. If it fails to start or its VM stops, or the app crashes before, the app goes back to the package it ran before (or the code it shipped with) and deletes the new one. A package runs as the installed app: its id, URL schemes and update source, and at most the capabilities the installed app declares. An app update with a newer build replaces the packages of older builds.

import { checkForPackage, downloadPackage, restartToPackage } from "nsh/updater";

const found = await checkForPackage();
if (found) {
  await downloadPackage();
  // Desktop: restart now. iOS and Android: it runs from the next start.
  await restartToPackage().catch(() => {});
}

#Calls in progress

One check, download or install runs at a time: calling any of them while another is in progress rejects. Disable the button while one runs.

import { openUrl, signal } from "nsh";
import { checkForUpdate, downloadUpdate, installAndRestart, type AppUpdate } from "nsh/updater";

const update = signal<AppUpdate | null>(null);
const busy = signal(false);

async function run<T>(work: () => Promise<T>) {
  busy.set(true);
  try {
    return await work();
  } finally {
    busy.set(false);
  }
}

export const check = () => run(async () => update.set(await checkForUpdate()));

export const install = () =>
  run(async () => {
    const found = update();
    if (!found) return;
    if (!found.canInstall) return openUrl(found.url);
    await downloadUpdate();
    await installAndRestart();
  });

#nsh.json

"updater" names one source: github, an owner/repo whose latest GitHub Release holds latest.json, or url, the manifest on a host of your own, which requires publicKey. publicKey is the contents of an nsh keygen .pub file; with it, unsigned manifests are refused.

{
  "id": "com.example.notes",
  "version": "1.2.0",
  "updater": { "github": "example/notes", "publicKey": "<contents of keys/update.pub>" }
}
Key Type Meaning
github string owner/repo: the manifest is the asset latest.json of its latest release. Drafts and prereleases are skipped
url string The manifest’s HTTPS URL on your own host; archives’ relative URLs resolve next to it
publicKey string The Ed25519 key that signs the manifests (latest.json.sig). Required with url
packages.roots string[] Root public keys (nsh keygen’s root.pub) whose certified release keys sign the app’s OTA packages. Without it the app runs no packages

nsh package checks the keys and adds them, with the app’s build, to the package, so the source is fixed when the app is built. version must be semver. nsh update-manifest writes latest.json and its signature for one release’s archives and package (CLI).

#Rust

Rust apps (nsh.json "root") use the nsh-updater crate directly with an HTTP client of their own: Config::check verifies the manifest, Download the archive, Installation::install replaces the app, and nsh_core::element::desktop::restart() starts the new version (App Updates).