README

moonbit-community/proton/updater does not have a README file

#
ArtifactFetch

type ArtifactFetch = async (String, Int64, async (Bytes) -> Unit raise UpdateError) -> Unit raise UpdateError

Streams an artifact into a consumer without retaining the whole body.

The expected size is authenticated manifest data. A transport may use it to reject a mismatching response header early; UpdateChannel independently enforces it against the chunks actually delivered.

#
Fetch

type Fetch = async (String) -> Bytes raise UpdateError

Fetches the whole body at a URL.

The network is a parameter rather than a dependency so that the sequence below — verify, decode, compare, verify again — can be exercised end to end without one. That sequence is the part worth testing exhaustively; an HTTP client is not.

#
UpdateError

pub(all) suberror UpdateError {
FetchFailed(url~ : String, detail~ : String)
ManifestSignatureInvalid
ManifestUndecodable(detail~ : String)
ManifestStale(published_at~ : String, oldest_accepted~ : String)
MalformedSignature(url~ : String)
UntrustedKey(detail~ : String)
ArtifactSizeMismatch(expected~ : Int64, actual~ : Int64)
ArtifactDigestMismatch(expected~ : String, actual~ : String)
ArtifactSignatureInvalid
ClockUnusable(detail~ : String)
InstallBusy
RollbackRejected(target~ : UInt64, installed~ : UInt64)
RevisionMismatch(detail~ : String)
InstallInterrupted(detail~ : String)
InstallFailed(step~ : String, detail~ : String)
} derive(Eq,
Debug
)

A reason an update was not accepted.

Every variant is a refusal. There is no variant meaning "checked and could not tell": a check that cannot reach a conclusion is a check that failed, and the caller must treat it as such.

#
UpdateError::message

fn UpdateError::message(self : UpdateError) -> String

#
AvailableUpdate

pub struct AvailableUpdate {
version :
Version

revision : UInt64
notes_url : String?
url : String
size : Int64
sha256_hex : String
signature_hex : String
} derive(Eq,
Debug
)

A release that passed every check short of being downloaded.

#
AvailableUpdate::notes_url

fn AvailableUpdate::notes_url(self : AvailableUpdate) -> String?

#
AvailableUpdate::revision

fn AvailableUpdate::revision(self : AvailableUpdate) -> UInt64

#
AvailableUpdate::size

fn AvailableUpdate::size(self : AvailableUpdate) -> Int64

#
AvailableUpdate::url

fn AvailableUpdate::url(self : AvailableUpdate) -> String

#
CheckOutcome

pub enum CheckOutcome {
UpToDate
Available(AvailableUpdate)
} derive(Eq,
Debug
)

What a check concluded.

#
InstallOutcome

pub enum InstallOutcome {
Installed
AlreadyInstalled
} derive(Eq,
Debug
)

#
UpdateChannel

pub struct UpdateChannel {
endpoint : String
trusted_keys : Array[
PublicKey
]
platform : String
current_revision : UInt64
}

An update channel, resolved from configuration.

#
UpdateChannel::check

async fn UpdateChannel::check(self : UpdateChannel, fetch : async (String) -> Bytes raise UpdateError, oldest_accepted :
Timestamp
) -> CheckOutcome raise UpdateError

Checks the channel for a newer release.

oldest_accepted is the earliest publication instant this client will believe. It is supplied rather than derived from a clock inside this function: the calendar arithmetic that turns a freshness window into an instant belongs where a date library is available, and keeping it out means the whole check can be tested by naming the instant directly.

The order of the steps is the point. The signature is checked before the document is parsed, so no attacker-supplied structure is interpreted until it is known to come from a trusted key. Freshness and revision are checked after, because both read fields that only the signature makes trustworthy.

#
UpdateChannel::download_and_install

async fn UpdateChannel::download_and_install(self : UpdateChannel, fetch : async (String, Int64, async (Bytes) -> Unit raise UpdateError) -> Unit raise UpdateError, update : AvailableUpdate, on_progress? : async (Int64) -> Unit noraise) -> InstallOutcome raise UpdateError

Streams, authenticates and installs an available update.

The native stage owns a private archive from the first chunk through expansion. download_into authenticates the same chunks written there, and any transfer or authentication failure discards the stage before returning.

The native stage is created beside the running application bundle. This is what keeps final replacement on one filesystem when the application is installed on an external volume. Neither the archive path nor the expanded bundle path crosses the FFI boundary, so verification cannot be separated from use by replacing a path after it was checked.

The application is not restarted. When to do that is a question about the user's unsaved work, not about the update, so it belongs to the caller — see relaunch.

#
UpdateChannel::new

fn UpdateChannel::new(endpoint : String, public_keys : Array[String], platform : String, current_revision : UInt64) -> UpdateChannel raise UpdateError

Resolves a channel from its configured form.

Trusted keys are parsed once, here, so that a key which cannot be used is reported when the channel is built rather than at the moment a signature needs checking.

#
http_fetch

async fn http_fetch(url : String) -> Bytes raise UpdateError

Fetches update metadata over HTTPS.

Plain HTTP is refused. The signature is what makes a response trustworthy, so TLS is not what protects this exchange — but there is no reason to advertise which release a user is running to anyone on the path, and no reason to accept a redirect into cleartext.

#
http_fetch_artifact

async fn http_fetch_artifact(url : String, expected_size : Int64, consume : async (Bytes) -> Unit raise UpdateError) -> Unit raise UpdateError

Streams an artifact over HTTPS without retaining its body.

expected_size comes from the authenticated manifest. A Content-Length mismatch can therefore be refused before reading the body, but the consumer still counts actual chunks because the header may be absent after content decoding or on a chunked response.

#
maximum_metadata_bytes

let maximum_metadata_bytes : Int

The largest body this client will accept from an update endpoint.

A manifest and a signature are both small. A server that offers something enormous in their place is either broken or hostile, and either way the client should stop rather than fill memory on the way to a signature check it was going to fail.

#
oldest_accepted

fn oldest_accepted(freshness_days : Int) ->
Timestamp
raise UpdateError

The earliest publication instant this client will believe, from the system clock.

Kept out of UpdateChannel::check on purpose. That function is the one sequence where the order of the steps is the security property, and a clock read inside it would make the whole sequence depend on when the test ran. Here the clock is read once and the result is data.

#
relaunch

fn relaunch() -> Unit raise UpdateError

Starts the installed replacement.

The caller is expected to exit once this returns. Two copies of the same application running against the same state is a worse outcome than a moment with none, and the new process is already starting by the time this returns.