JuliusBrussee/caveman · error · Error

${label} key_id is required

Error message

${label} key_id is required

What it means

decodeReceiptKey validates every keyring entry used to verify signed usage receipts. key_id must be a non-empty (after trim) string because each receipt's signature must be attributable to a named key; a missing, empty, or whitespace-only key_id aborts verification. The label in the message identifies the failing entry, e.g. `keys[2]`.

Source

Thrown at packages/cli/src/index.ts:17392

  const sorted = [...receipts].sort((a, b) => a.seq - b.seq);
  let prev: Receipt | undefined;
  for (const r of sorted) {
    const decoded = keys.get(r.signature?.key_id);
    if (!decoded) return `seq ${r.seq}: no trusted public key for key_id ${String(r.signature?.key_id)}`;
    const err = verifyReceipt(r, decoded.key, decoded.info.key_id);
    if (err) return err;
    if (prev) {
      if (r.seq !== prev.seq + 1) return `seq ${r.seq}: not strictly after ${prev.seq}`;
      if (r.prev_receipt_hash !== prev.receipt_hash) return `seq ${r.seq}: prev_receipt_hash does not link to seq ${prev.seq}`;
      if (r.day <= prev.day) return `seq ${r.seq}: day ${r.day} does not follow ${prev.day}`;
    }
    prev = r;
  }
  return null;
}

function decodeReceiptKey(info: ReceiptPublicKey, label: string): DecodedReceiptKey {
  if (!info || typeof info.key_id !== "string" || !info.key_id.trim()) throw new Error(`${label} key_id is required`);
  if (info.alg !== "Ed25519") throw new Error(`${label} has unsupported algorithm ${String(info.alg)}`);
  if (typeof info.key !== "string" || !info.key.trim()) throw new Error(`${label} key is required`);
  const raw = Buffer.from(info.key, "base64");
  if (raw.length !== 32 || raw.toString("base64") !== info.key) throw new Error(`${label} must be a canonical base64 Ed25519 public key`);
  return { info, raw, key: ed25519PublicKey(raw) };
}

function decodeUniqueKeyring(infos: ReceiptPublicKey[], label: string): Map<string, DecodedReceiptKey> {
  const keys = new Map<string, DecodedReceiptKey>();
  for (const [index, info] of infos.entries()) {
    const decoded = decodeReceiptKey(info, `${label}[${index}]`);
    if (keys.has(decoded.info.key_id)) throw new Error(`${label} contains duplicate key_id ${decoded.info.key_id}`);
    keys.set(decoded.info.key_id, decoded);
  }
  return keys;
}

function embeddedReceiptKeys(bundle: ReceiptBundle): { current: DecodedReceiptKey; keys: Map<string, DecodedReceiptKey> } {

View on GitHub (pinned to 5184b3d11a)

Solutions

  1. Inspect the entry named by the label in the error message
  2. Set key_id to the issuer's published non-empty identifier
  3. Regenerate the keyring from the issuer's canonical source instead of editing it by hand

Example fix

// before
{ "alg": "Ed25519", "key": "MCowBQYDK2VwAyEA..." }
// after
{ "key_id": "caveman-2026-q3", "alg": "Ed25519", "key": "MCowBQYDK2VwAyEA..." }
Defensive patterns

Strategy: type-guard

Validate before calling

const valid = keyring.every((k) => typeof k?.key_id === 'string' && k.key_id.trim().length > 0);
if (!valid) throw new Error('keyring has entries without key_id — fetch the canonical keyring');

Type guard

const hasKeyId = (k: unknown): k is { key_id: string } =>
  typeof (k as { key_id?: unknown })?.key_id === 'string' &&
  (k as { key_id: string }).key_id.trim() !== '';

Prevention

When it happens

Trigger: A receipts keyring JSON with an entry whose key_id is absent, empty, or whitespace; programmatically generated keyrings that skip the id field; hand-merging keyrings from multiple sources.

Common situations: Hand-edited keyring files; an upstream publishing pipeline dropping key_id during serialization; schema drift between the issuer's keyring format and the verifier's expectations.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18). Data as JSON: /api/errors/679d6c85ae14d141. Report an issue: GitHub.