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
- Verify the key id format supplied to the revocation API
- Reject/fix the caller producing malformed key ids
- 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)