{"record":{"id":"e57290a2d1728b82","repo":"astrid-runtime/astrid","slug":"signed-channel-lifetime-is-invalid","errorCode":null,"errorMessage":"signed channel lifetime is invalid","messagePattern":"signed channel lifetime is invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/update_channel.rs","lineNumber":481,"sourceCode":"        pointer.schema_version == 1\n            && pointer.kind == \"astrid-channel\"\n            && pointer.product == PRODUCT\n            && pointer.repository == REPOSITORY,\n        \"signed channel identity is invalid\"\n    );\n    ensure!(\n        pointer.channel == expected_channel.as_str(),\n        \"signed channel names '{}', expected '{}'\",\n        pointer.channel,\n        expected_channel.as_str()\n    );\n    ensure!(\n        pointer.generation > 0,\n        \"signed channel generation must be positive\"\n    );\n    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\"","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/update_channel.rs#L463-L499","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (channel.toml)\npublished-at = \"2026-09-01T00:00:00Z\"\nexpires-at = \"2026-09-01T00:00:00Z\"\n// after\npublished-at = \"2026-09-01T00:00:00Z\"\nexpires-at = \"2026-09-02T00:00:00Z\"","handlingStrategy":"validation","validationCode":"fn lifetime_is_positive(p: &ChannelPointer) -> bool {\n    p.expires_at > p.published_at\n}","typeGuard":"fn has_positive_lifetime(p: &ChannelPointer) -> bool {\n    p.expires_at > p.published_at\n}","tryCatchPattern":"match parse_channel(&bytes, channel, now) {\n    Err(e) if e.to_string().contains(\"lifetime is invalid\") => {\n        anyhow::bail!(\"channel pointer timestamps are inconsistent (expires <= published); republish\")\n    }\n    other => other?,\n}","preventionTips":["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"],"tags":["validation","update-channel","timestamp"],"backgroundTag":"invalid-date-format","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"}