JuliusBrussee/caveman · error
v2 bundle requires public_keys
Error message
v2 bundle requires public_keys
What it means
Schema "caveman.receipt-bundle.v2" requires a non-empty public_keys array (the rotation keyring). A v2 bundle with public_keys absent, not an array, or an empty array is rejected before any key is decoded.
Source
Thrown at packages/cli/src/index.ts:17416
}
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> } {
if (bundle.schema !== RECEIPT_BUNDLE_V1 && bundle.schema !== RECEIPT_BUNDLE_V2) throw new Error(`unsupported bundle schema ${String(bundle.schema)}`);
if (bundle.verification_coverage !== undefined && bundle.verification_coverage !== INCLUDED_RECEIPTS_ONLY) throw new Error(`unsupported unsigned verification coverage ${String(bundle.verification_coverage)}`);
if (bundle.completeness_attested === true) throw new Error("bundle completeness cannot be attested by unsigned export metadata");
const current = decodeReceiptKey(bundle.public_key, "public_key");
if (bundle.public_keys !== undefined && !Array.isArray(bundle.public_keys)) throw new Error("public_keys must be an array");
if (bundle.schema === RECEIPT_BUNDLE_V2 && (!Array.isArray(bundle.public_keys) || bundle.public_keys.length === 0)) throw new Error("v2 bundle requires public_keys");
const keys = decodeUniqueKeyring(bundle.public_keys ?? [], "public_keys");
const currentInRing = keys.get(current.info.key_id);
if (currentInRing && !currentInRing.raw.equals(current.raw)) throw new Error(`public_key conflicts with public_keys entry ${current.info.key_id}`);
if (bundle.schema === RECEIPT_BUNDLE_V2 && !currentInRing) throw new Error("v2 public_keys must include public_key");
if (!currentInRing) keys.set(current.info.key_id, current);
return { current, keys };
}
async function pinnedReceiptKeys(file: string, current: DecodedReceiptKey): Promise<{ keys: Map<string, DecodedReceiptKey>; trust: string }> {
const source = (await readFile(file, "utf8")).trim();
if (!source.startsWith("{")) {
const pinned = decodeReceiptKey({ ...current.info, key: source }, "--pubkey");
if (!pinned.raw.equals(current.raw)) throw new Error("bundle public key does not match the published --pubkey");
return { keys: new Map([[current.info.key_id, pinned]]), trust: "pinned_public_key" };
}
let parsed: { public_key?: ReceiptPublicKey; public_keys?: ReceiptPublicKey[] };
try { parsed = JSON.parse(source); } catch { throw new Error("--pubkey JSON is malformed"); }
const infos = Array.isArray(parsed.public_keys) ? parsed.public_keys : parsed.public_key ? [parsed.public_key] : [];View on GitHub (pinned to 5184b3d11a)
Solutions
- Include a non-empty public_keys array containing at least the current signing key
- Or export as schema "caveman.receipt-bundle.v1" if you have a single key and no rotation keyring
- Regenerate with the current producer version so v2 exports always carry the keyring
Example fix
// before
{ "schema": "caveman.receipt-bundle.v2", "public_key": {...} }
// after
{ "schema": "caveman.receipt-bundle.v2", "public_key": {...}, "public_keys": [ {...current key...} ] } Defensive patterns
Strategy: validation
Validate before calling
if (bundle.schema === "caveman.receipt-bundle.v2") {
if (!Array.isArray(bundle.public_keys) || bundle.public_keys.length === 0) {
throw new Error("v2 bundle needs a non-empty public_keys keyring");
}
} Type guard
function isCompleteV2Bundle(b: { schema?: unknown; public_keys?: unknown }): boolean {
return b.schema !== "caveman.receipt-bundle.v2" || (Array.isArray(b.public_keys) && b.public_keys.length > 0);
} Try / catch
try { execSync(`caveman receipts verify ${bundle}`); }
catch (e) {
if (/v2 bundle requires public_keys/.test(String((e as Error).message))) fail("re-export: v2 needs the keyring");
throw e;
} Prevention
- When bumping schema to v2, update the exporter to emit public_keys in the same change
- Validate v2 exports in producer tests: keyring present and non-empty
- Use v1 only when you genuinely have a single non-rotating key
When it happens
Trigger: A producer emits a v1-shaped bundle (only the top-level public_key) but labels it schema v2; or the keyring was emptied during editing before export.
Common situations: Bumping the schema string to v2 without updating export code to emit the keyring; hand-editing the schema field; test fixtures copied from v1 bundles.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- v2 public_keys must include public_key
- ${label} key_id is required
- ${label} key is required
- ${label} contains duplicate key_id ${decoded.info.key_id}
- public_keys must be an array
AI-assisted analysis of JuliusBrussee/caveman@5184b3d11a (2026-08-18).
Data as JSON: /api/errors/1e7189238849e97e.
Report an issue: GitHub.