Parses and validates release-manifest.json.
Source of truth: docs/manifest-format.md. This module owns schema
validation and the two policy checks (counter, expiry) the device
runs after signature verification; it does not verify the signature
itself (see NervesGithubUpdater.Signature) or touch the
filesystem/network.
Library-shape: pure functions, no host dependencies, no processes.
Counter semantics
counter is a monotonic anchor persisted by the caller in
Nerves.Runtime.KV (fw_manifest_counter) after a successful
flash. check_counter/2 allows the manifest counter to be equal
to the stored one (reinstalling the current release), rejects a
lower one (rollback), and accepts when nothing is stored yet
(first-contact trust — same model as the public key baked into
firmware). A stored value that fails to parse as an integer is
treated as absent rather than raising: a corrupted KV anchor must
not permanently brick the update path.
Expiry is policy-gated
expires_at only matters when the caller opts in via
enforce_expiry: true (device-side default off). A dormant project
— no CI running, no one rotating expires_at forward — must not
have its devices' update path silently brick itself just because a
manifest aged out.
Summary
Functions
Rejects a manifest whose counter is lower than the persisted anchor.
Rejects an expired manifest, but only when enforce? is true.
Parses and validates binary as a version-1 release manifest.
Looks up a target entry by Nerves platform name.
Types
@type t() :: %NervesGithubUpdater.Manifest{ counter: non_neg_integer(), deltas: map(), expires_at: DateTime.t() | nil, signed_at: DateTime.t(), targets: %{required(String.t()) => target()}, version: 1 }
@type target() :: %{asset: String.t(), sha256: String.t(), size: non_neg_integer()}
Functions
@spec check_counter(t(), String.t() | nil) :: :ok | {:error, {:manifest_rollback, non_neg_integer(), integer()}}
Rejects a manifest whose counter is lower than the persisted anchor.
stored is the raw string read from Nerves KV — nil, "", or a
string that fails to parse as an integer are all treated as "no
anchor yet" and accepted (see moduledoc).
@spec check_expiry(t(), boolean(), DateTime.t()) :: :ok | {:error, {:manifest_expired, DateTime.t()}}
Rejects an expired manifest, but only when enforce? is true.
Parses and validates binary as a version-1 release manifest.
Returns {:ok, t()} or {:error, {:invalid_manifest, reason}} on
any schema violation (bad JSON, unsupported version, missing/invalid
fields, empty or malformed targets).
Looks up a target entry by Nerves platform name.