astrid-runtime/astrid · error
signed channel lifetime is invalid
Error message
signed channel lifetime is invalid
What it means
The pointer's `expires_at` timestamp must be strictly after its `published_at` timestamp (both parsed by canonical_time). A non-positive lifetime means the metadata is self-inconsistent — it expired before or exactly when it was published — so it is rejected before any freshness checks run.
Solutions
- Ensure `expires_at` is strictly later than `published_at` (e.g. published_at + channel max lifetime)
- Check both fields are in the same format/timezone expected by canonical_time (RFC 3339/UTC)
- Re-generate the pointer with the official publishing tool rather than editing timestamps by hand
- If only expiry passed too early, republish the channel with a corrected expiry
Example fix
// before (channel.toml) published-at = "2026-09-01T00:00:00Z" expires-at = "2026-09-01T00:00:00Z" // after published-at = "2026-09-01T00:00:00Z" expires-at = "2026-09-02T00:00:00Z"
Defensive patterns
Strategy: validation
Validate before calling
fn lifetime_is_positive(p: &ChannelPointer) -> bool {
p.expires_at > p.published_at
} Type guard
fn has_positive_lifetime(p: &ChannelPointer) -> bool {
p.expires_at > p.published_at
} Try / catch
match parse_channel(&bytes, channel, now) {
Err(e) if e.to_string().contains("lifetime is invalid") => {
anyhow::bail!("channel pointer timestamps are inconsistent (expires <= published); republish")
}
other => other?,
} Prevention
- Compute expires_at as published_at + duration in one place in the publisher
- Serialize both timestamps in the same UTC RFC 3339 format
- Never hand-edit timestamp fields in signed metadata
- Add a unit test asserting expires > published for every generated pointer
When it happens
Trigger: parse_channel or enforce_continuity reads a pointer where `expires_at <= published_at`, e.g. `expires_at` copied from `published_at`, a swapped field order, or a publisher computing expiry with an off-by-zero duration.
Common situations: Hand-edited channel TOML with swapped timestamps; a publishing script using the wrong variable for expiry; timezone/UTC conversion mistakes shifting one timestamp by hours so ordering flips.
Related errors
- signed channel lifetime exceeds the maximum for its channel
- must contain exactly targets
- nightly channel must point to an exact nightly prerelease
- signed channel generation must be positive
- signed channel names
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/e57290a2d1728b82.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-cli/src/commands/update_channel.rs:481
pointer.schema_version == 1
&& pointer.kind == "astrid-channel"
&& pointer.product == PRODUCT
&& pointer.repository == REPOSITORY,
"signed channel identity is invalid"
);
ensure!(
pointer.channel == expected_channel.as_str(),
"signed channel names '{}', expected '{}'",
pointer.channel,
expected_channel.as_str()
);
ensure!(
pointer.generation > 0,
"signed channel generation must be positive"
);
let published = canonical_time(&pointer.published_at, "published-at")?;
let expires = canonical_time(&pointer.expires_at, "expires-at")?;
ensure!(expires > published, "signed channel lifetime is invalid");
if let Some(now) = now {
ensure!(now <= expires, "signed channel metadata has expired");
let latest_reasonable_publication = now
.checked_add_signed(chrono::Duration::seconds(MAX_FUTURE_SKEW_SECS))
.context("channel publication skew overflowed the clock")?;
ensure!(
published <= latest_reasonable_publication,
"signed channel published-at is unreasonably far in the future"
);
}
let max_lifetime = match expected_channel {
UpdateChannel::Stable => chrono::Duration::days(30),
UpdateChannel::Dev => chrono::Duration::days(7),
UpdateChannel::Nightly => chrono::Duration::days(2),
};
ensure!(
expires.signed_duration_since(published) <= max_lifetime,
"signed channel lifetime exceeds the maximum for its channel"View on GitHub (pinned to affd8760f4)