{"record":{"id":"f7bdf9325e9a5969","repo":"astrid-runtime/astrid","slug":"signed-channel-metadata-has-expired","errorCode":null,"errorMessage":"signed channel metadata has expired","messagePattern":"signed channel metadata has expired","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/update_channel.rs","lineNumber":483,"sourceCode":"            && 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\"\n    );\n    let version = canonical_version(&pointer.release.version)?;","sourceCodeStart":465,"sourceCodeEnd":501,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/update_channel.rs#L465-L501","documentation":"When `now` is supplied (always via parse_channel), the pointer must not yet be expired: the check requires `now <= expires_at`. Channel pointers are deliberately short-lived signed attestations; an expired pointer means the channel has not been republished within its validity window, so the client refuses to trust it for updates.","triggerScenarios":"parse_channel(bytes, expected_channel, now) at a time later than the pointer's `expires_at`; calling against a stale mirror/cache; the publishing pipeline for that channel silently stopped producing new pointers.","commonSituations":"Client machine clock far ahead (wrong system time/RTC); a frozen CI cache or offline mirror serving an old channel.toml; the release automation has been broken for days so nobody republished stable.","solutions":["Re-fetch the channel pointer from the authoritative feed to get a fresh, unexpired publication","Check the local system clock (`date`/NTP sync) — a clock far in the future makes valid pointers look expired","If you operate the feed, fix and re-run the channel publishing workflow to renew expiry","Clear any CDN/proxy cache that is serving stale channel metadata"],"exampleFix":"// before: using a stale cached pointer\nlet bytes = std::fs::read(\"cache/stable-channel.toml\")?;\nlet ptr = parse_channel(&bytes, channel, Utc::now())?; // expired\n// after: refresh from upstream first\nlet bytes = source.fetch_channel(UpdateChannel::Stable).await?;\nlet ptr = parse_channel(&bytes, channel, Utc::now())?;","handlingStrategy":"retry","validationCode":"fn pointer_is_current(p: &ChannelPointer, now: chrono::DateTime<Utc>) -> bool {\n    now <= p.expires_at\n}","typeGuard":"fn not_expired(p: &ChannelPointer, now: chrono::DateTime<Utc>) -> bool {\n    now <= p.expires_at\n}","tryCatchPattern":"let ptr = match parse_channel(&bytes, channel, Utc::now()) {\n    Err(e) if e.to_string().contains(\"has expired\") => {\n        warn!(\"channel pointer expired; refetching\");\n        let fresh = feed.fetch_channel(channel).await?;\n        parse_channel(&fresh, channel, Utc::now())?\n    }\n    other => other?,\n};","preventionTips":["Always fetch channel metadata fresh from upstream; avoid long-lived local caches","Keep the system clock NTP-synced","Monitor the publishing pipeline so expired channels are noticed before users hit them","Set cache/CDN TTLs well below the channel's max lifetime"],"tags":["update-channel","expired-metadata","time"],"backgroundTag":"jwt-token-expired","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"}