astrid-runtime/astrid · error

invalid device revocation key id

Error message

invalid device revocation key id

What it means

Validation guard in record_device_max: the device revocation key id supplied with the revocation request does not parse as a valid key id, so the maximum-epoch fence cannot be recorded durably. This is an input-shape guard on the revocation record before it is written to the KV store via the CAS/max path; apply_device_revocation surfaces it as a request error.

Solutions

  1. Verify the key id format supplied to the revocation API
  2. Reject/fix the caller producing malformed key ids
  3. Check for KV entries written by older versions with an incompatible key format
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at crates/astrid-gateway/src/revocations.rs:188 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/8bb9d5709b8bee4e. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-gateway/src/revocations.rs:188

    }
}

/// Record the maximum device revocation epoch durably using the same CAS/max
/// rule as principal revocations.
///
/// A successful CAS, or a successful maximum-epoch fallback write after a
/// CAS error, leaves a fence that startup hydration can restore. The function
/// still returns an error after any CAS error so the HTTP caller withholds
/// `204`, even when the fallback tombstone succeeded. If both writes fail,
/// there is no durable fence; the caller may install a process-local maximum,
/// but a later healthy empty KV cannot reconstruct it.
pub async fn record_device_max(
    store: &dyn KvStore,
    key_id: &str,
    epoch: u64,
) -> anyhow::Result<u64> {
    if key_id.is_empty() || key_id.contains('/') {
        anyhow::bail!("invalid device revocation key id");
    }
    let key = format!("{DEVICE_PREFIX}{key_id}");
    loop {
        let current = store
            .get(REVOCATION_NAMESPACE, &key)
            .await
            .map_err(|error| anyhow::anyhow!("read device revocation {key_id}: {error}"))?;
        let current_epoch = current
            .as_deref()
            .map(|bytes| decode_epoch(bytes, &key))
            .transpose()?;
        let wanted = current_epoch.map_or(epoch, |current| current.max(epoch));
        if current_epoch == Some(wanted) {
            return Ok(wanted);
        }
        match store
            .compare_and_swap(
                REVOCATION_NAMESPACE,

View on GitHub (pinned to affd8760f4)