proton_updater

Update manifest schema shared by the Proton runtime and CLI.

proton
updater
manifest
moon add moonbit-community/proton_updater@0.1.16
Download zip
Version
0.1.16
License
Apache-2.0
Last updated
3 days ago
Downloads
40
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, and unknown fields are an error rather than something to skip — the same discipline proton.project.json and the native runtime configs already follow, so that a typo in a release manifest fails loudly instead of silently omitting whatever it was meant to say.

One case deliberately does not fail: a platform entry whose kind this client does not implement is treated as no update on offer. Failing the whole document would let a newer release format lock every older client out of every platform at once, including the platforms it could still have served.

#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::message

fn ManifestError::message(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.

A platform that is absent, or whose entry names a kind this client does not implement, has no update on offer. Neither is an error: a manifest may legitimately describe artifacts that a older client cannot install.

#
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::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::is_before

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

Returns whether this instant precedes other.

#
Timestamp::parse

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

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

#
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::is_newer_than

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

Returns whether this version is strictly newer than other.

#
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.

#
supported_schema_version

let supported_schema_version : Int

The only manifest schema this client understands.