proton_updater

Update manifest schema shared by the Proton runtime and CLI.

proton
updater
manifest
moon add moonbit-community/proton_updater@0.2.1
Download zip
Version
0.2.1
License
Apache-2.0
Last updated
7 hours ago
Downloads
468
README

#moonbit-community/proton_updater

The update manifest schema, shared by the Proton runtime and proton_cli.

The runtime reads manifests; the CLI writes them. Putting the schema in its own module keeps one definition of the format rather than two that drift, and lets the CLI depend on it without depending on the whole runtime.

#Scope

Decoding and ordering. This module has no network access, no filesystem access, and no cryptography — it does not verify signatures, only carries them. That separation is deliberate: decoding a manifest proves it is well formed and nothing more, and a type that could be mistaken for a trusted manifest is worse than one that obviously is not.

The caller is responsible, in order, for: verifying the manifest signature, rejecting a revision that is not strictly newer than the installed one, and rejecting a manifest older than its freshness window.

#Decoding

let manifest = @updater.Manifest::parse(text)
match manifest.platform("darwin-arm64") {
Some(update) => download(update.url(), update.size())
None => () // Nothing on offer for this platform.
}

schema_version must be exactly 2, kind must be "full", and unknown fields are errors. A release manifest is one strict protocol document: invalid or unsupported content fails loudly instead of being treated as no update.

#Ordering

revision is a positive unsigned 64-bit release sequence. It is the security ordering: every published update increments it, and it never resets when the display version changes. Keeping it separate lets applications use ordinary version labels without making rollback prevention depend on semantic-version policy.

Version is exactly three non-negative integers, with no leading zeros and no pre-release or build suffix. It is display metadata, not the install ordering; the strict shape keeps manifests predictable while revision decides whether an artifact may replace the installed application.

Timestamp accepts only YYYY-MM-DDTHH:MM:SSZ. Every field is fixed width in that form, so lexicographic order is chronological order and is_before needs no calendar arithmetic. Numeric offsets are refused because comparing them would require converting them, and a freshness check that silently mis-converts is a check that has quietly stopped working.

#Encodings

sha256 and signature are hexadecimal, lowercase, and strictly validated: one encoding across the manifest and the key format means one strict decoder rather than two that can disagree.

Artifact URLs must be https. The signature is checked regardless, so this is not what makes an update safe; it removes an opportunity rather than a defence.

#
ManifestError

pub(all) suberror ManifestError {
NotJson
NotAnObject(path~ : String)
UnknownSchemaVersion(found~ : Int)
UnknownField(path~ : String)
MissingField(path~ : String)
InvalidField(path~ : String, expectation~ : String)
} derive(Eq,
Debug
)

An update manifest could not be decoded.

Every variant names the field at fault, because a manifest is written by a release process and read by a client that cannot ask for a correction.

#
ManifestError::equal

#
ManifestError::message

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

#
ManifestError::not_equal

fn ManifestError::not_equal(x : ManifestError, y : ManifestError) -> Bool

#
ManifestError::output

fn ManifestError::output(self : ManifestError, logger : &Logger) -> Unit

#
ManifestError::to_string

fn ManifestError::to_string(self : ManifestError) -> String

#
Manifest

pub struct Manifest {
version : Version
revision : UInt64
published_at : Timestamp
notes_url : String?
platforms : Map[String, PlatformUpdate]
}

A decoded update manifest.

Decoding proves the document is well formed. It proves nothing about authenticity: the caller verifies the manifest signature before trusting any of this, and enforces revision monotonicity and freshness afterwards.

#
Manifest::from_json

fn Manifest::from_json(json : Json) -> Manifest raise ManifestError

Decodes a manifest from an already parsed JSON value.

#
Manifest::notes_url

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

#
Manifest::parse

fn Manifest::parse(text : String) -> Manifest raise ManifestError

Decodes a manifest document.

#
Manifest::platform

fn Manifest::platform(self : Manifest, platform : String) -> PlatformUpdate?

Returns the update offered for one platform identifier, if any.

#
Manifest::platforms

fn Manifest::platforms(self : Manifest) -> Array[String]

Returns every platform identifier this manifest offers an update for.

#
Manifest::published_at

fn Manifest::published_at(self : Manifest) -> Timestamp

#
Manifest::revision

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

The signed, monotonically increasing release order.

Unlike the display version, this value is compared at install time and never resets. It is what prevents an older signed release from replacing a newer installed application.

#
Manifest::version

fn Manifest::version(self : Manifest) -> Version

#
PlatformUpdate

pub struct PlatformUpdate {
url : String
size : Int64
sha256_hex : String
signature_hex : String
} derive(Eq,
Debug
)

One platform's full-artifact update.

#
PlatformUpdate::equal

#
PlatformUpdate::not_equal

fn PlatformUpdate::not_equal(x : PlatformUpdate, y : PlatformUpdate) -> Bool

#
PlatformUpdate::sha256_hex

fn PlatformUpdate::sha256_hex(self : PlatformUpdate) -> String

#
PlatformUpdate::signature_hex

fn PlatformUpdate::signature_hex(self : PlatformUpdate) -> String

#
PlatformUpdate::size

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

#
PlatformUpdate::url

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

#
Timestamp

pub struct Timestamp {
text : String
} derive(Eq,
Debug
)

A publication instant, stored as the exact text it was written with.

Only YYYY-MM-DDTHH:MM:SSZ is accepted: fixed width, UTC, no fractional seconds and no numeric offset. Every field is fixed width in that form, so lexicographic order is chronological order and comparison needs no calendar arithmetic at all.

Accepting offsets would mean converting before comparing, and a freshness check that silently mis-converts is a check that quietly stops working.
impl Show for Timestamp

#
Timestamp::equal

fn Timestamp::equal(Timestamp, Timestamp) -> Bool

#
Timestamp::is_before

fn Timestamp::is_before(self : Timestamp, other : Timestamp) -> Bool

Returns whether this instant precedes other.

#
Timestamp::not_equal

fn Timestamp::not_equal(x : Timestamp, y : Timestamp) -> Bool

#
Timestamp::output

fn Timestamp::output(self : Timestamp, logger : &Logger) -> Unit

#
Timestamp::parse

fn Timestamp::parse(text : String) -> Timestamp?

Parses YYYY-MM-DDTHH:MM:SSZ, validating field ranges.

#
Timestamp::to_string

fn Timestamp::to_string(self : Timestamp) -> String

#
Timestamp::to_text

fn Timestamp::to_text(self : Timestamp) -> String

Returns the text this timestamp was parsed from.

#
Version

pub struct Version {
major : Int
minor : Int
patch : Int
} derive(Eq,
Debug
)

A release version, as three non-negative integers.

The updater offers an update only when the manifest version is strictly greater than the running one, so ordering is a security control rather than a display concern: a version that compares wrongly is a rollback.

Exactly three components are required, and pre-release or build suffixes are rejected. Ordering suffixed versions correctly is subtle — 1.0.0-rc.1 precedes 1.0.0, and -rc.10 follows -rc.2 — and a rule that is subtle in a comparison this important is better left out than approximated.
impl Show for Version

#
Version::equal

fn Version::equal(Version, Version) -> Bool

#
Version::is_newer_than

fn Version::is_newer_than(self : Version, other : Version) -> Bool

Returns whether this version is strictly newer than other.

#
Version::not_equal

fn Version::not_equal(x : Version, y : Version) -> Bool

#
Version::output

fn Version::output(self : Version, logger : &Logger) -> Unit

#
Version::parse

fn Version::parse(text : String) -> Version?

Parses major.minor.patch.

Each component must be decimal digits with no sign, no leading zero beyond the single digit 0, and no suffix. Leading zeros are refused so that one release cannot be written two ways.

#
Version::to_repr

#
Version::to_string

fn Version::to_string(self : Version) -> String

#
supported_schema_version

let supported_schema_version : Int

The only manifest schema this client understands.