{"record":{"id":"c7c7f54d5f2d3a8c","repo":"astrid-runtime/astrid","slug":"signed-channel-lifetime-exceeds-the-maximum-for-it","errorCode":null,"errorMessage":"signed channel lifetime exceeds the maximum for its channel","messagePattern":"signed channel lifetime exceeds the maximum for its channel","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/update_channel.rs","lineNumber":497,"sourceCode":"    let published = canonical_time(&pointer.published_at, \"published-at\")?;\n    let expires = canonical_time(&pointer.expires_at, \"expires-at\")?;\n    ensure!(expires > published, \"signed channel lifetime is invalid\");\n    if let Some(now) = now {\n        ensure!(now <= expires, \"signed channel metadata has expired\");\n        let latest_reasonable_publication = now\n            .checked_add_signed(chrono::Duration::seconds(MAX_FUTURE_SKEW_SECS))\n            .context(\"channel publication skew overflowed the clock\")?;\n        ensure!(\n            published <= latest_reasonable_publication,\n            \"signed channel published-at is unreasonably far in the future\"\n        );\n    }\n    let max_lifetime = match expected_channel {\n        UpdateChannel::Stable => chrono::Duration::days(30),\n        UpdateChannel::Dev => chrono::Duration::days(7),\n        UpdateChannel::Nightly => chrono::Duration::days(2),\n    };\n    ensure!(\n        expires.signed_duration_since(published) <= max_lifetime,\n        \"signed channel lifetime exceeds the maximum for its channel\"\n    );\n    let version = canonical_version(&pointer.release.version)?;\n    let nightly_commit = nightly_source_commit(&version);\n    match expected_channel {\n        UpdateChannel::Nightly => ensure!(\n            nightly_commit.is_some() && version.build.is_empty(),\n            \"nightly channel must point to an exact nightly prerelease\"\n        ),\n        UpdateChannel::Stable | UpdateChannel::Dev => ensure!(\n            version.pre.is_empty() && version.build.is_empty(),\n            \"stable and dev channels must point to canonical releases\"\n        ),\n    }\n    ensure!(\n        pointer.release.tag == format!(\"v{version}\"),\n        \"signed channel release tag does not match its version\"","sourceCodeStart":479,"sourceCodeEnd":515,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/update_channel.rs#L479-L515","documentation":"Each channel has a maximum allowed pointer lifetime: stable 30 days, dev 7 days, nightly 2 days. Even if expiry is after publication, a pointer valid longer than its channel's cap is rejected, because long-lived pointers undermine the short-expiry trust model (a compromised file would remain usable too long).","triggerScenarios":"parse_channel or enforce_continuity reads a pointer where `expires_at - published_at` exceeds the cap for `expected_channel`, e.g. a nightly with expires 30 days after publish.","commonSituations":"Publisher script reusing the stable expiry (30d) constant for nightly/dev builds; hand-edited expiry extended to 'keep updates working'; template files copied between channels without adjusting lifetimes.","solutions":["Set `expires_at = published_at + max_lifetime` for the channel (30d stable, 7d dev, 2d nightly) and republish","Parameterize the publishing script's expiry per channel instead of sharing one constant","If the pointer came from upstream and is oversized, report/fix the release automation and use a freshly generated pointer","Shorten the lifetime and re-sign the metadata"],"exampleFix":"// before: nightly published with stable-style expiry\nlet expires = published + Duration::days(30);\n// after\nlet expires = match channel {\n    UpdateChannel::Nightly => published + Duration::days(2),\n    UpdateChannel::Dev => published + Duration::days(7),\n    UpdateChannel::Stable => published + Duration::days(30),\n};","handlingStrategy":"validation","validationCode":"fn lifetime_within_cap(p: &ChannelPointer, channel: UpdateChannel) -> bool {\n    let days = match channel { UpdateChannel::Stable => 30, UpdateChannel::Dev => 7, UpdateChannel::Nightly => 2 };\n    p.expires_at.signed_duration_since(p.published_at) <= chrono::Duration::days(days)\n}","typeGuard":"fn lifetime_ok(p: &ChannelPointer, channel: &UpdateChannel) -> bool {\n    let max = match channel {\n        UpdateChannel::Stable => chrono::Duration::days(30),\n        UpdateChannel::Dev => chrono::Duration::days(7),\n        UpdateChannel::Nightly => chrono::Duration::days(2),\n    };\n    p.expires_at.signed_duration_since(p.published_at) <= max\n}","tryCatchPattern":"match parse_channel(&bytes, channel, now) {\n    Err(e) if e.to_string().contains(\"exceeds the maximum\") => {\n        anyhow::bail!(\"publisher misconfigured expiry for this channel; report to feed operator\")\n    }\n    other => other?,\n}","preventionTips":["Parameterize expiry duration per channel in the publishing tool","Never reuse stable's 30-day expiry template for dev/nightly","Add a publisher-side assertion mirroring validate_pointer's max_lifetime table"],"tags":["update-channel","validation","timestamp"],"backgroundTag":"value-out-of-range","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}