ruvnet/ruflo · error · Error
both Ed25519 private and public PEM keys are required
Error message
both Ed25519 private and public PEM keys are required
What it means
Thrown by createFlywheelReceipt() when exactly one of privateKeyPem / publicKeyPem is supplied. Receipt signing uses Ed25519: the private key signs the canonicalized payload and the public key is embedded in the receipt for verification, so both halves are required. Supplying neither produces a valid unsigned receipt; supplying only one is always a caller bug.
Source
Thrown at v3/@claude-flow/cli/src/services/flywheel-receipt.ts:398
evidence: input.evidence ?? {
corpusRoles: {
selectionTaskIds: [],
promotionHoldoutTaskIds: [],
guardTaskIds: [],
},
verification: {},
canary: {},
},
termVerification: input.termVerification ?? [],
decision,
issuedAt: new Date(now).toISOString(),
expiresAt: new Date(now + (input.ttlMs ?? 24 * 60 * 60 * 1000)).toISOString(),
} satisfies Omit<FlywheelReceiptPayload, 'receiptId'>;
const receiptId = sha256Ref(canonicalizeJcs(receiptIdentityPayload(base)));
const payload: FlywheelReceiptPayload = { ...base, receiptId };
const receipt: FlywheelEvaluationReceipt = { payload };
if (input.privateKeyPem || input.publicKeyPem) {
if (!input.privateKeyPem || !input.publicKeyPem) throw new Error('both Ed25519 private and public PEM keys are required');
receipt.signature = {
algorithm: 'ed25519',
domain: RECEIPT_DOMAIN,
publicKeyPem: input.publicKeyPem,
signatureBase64: edSign(null, signedBytes(payload), input.privateKeyPem).toString('base64'),
};
}
return receipt;
}
export interface ReceiptVerification {
valid: boolean;
signed: boolean;
errors: string[];
}
export function verifyFlywheelReceipt(receipt: FlywheelEvaluationReceipt, trustedPublicKeys?: Set<string>): ReceiptVerification {
const errors: string[] = [];View on GitHub (pinned to 6b01dc5a68)
Solutions
- Pass BOTH privateKeyPem and publicKeyPem, or pass NEITHER for an unsigned receipt.
- Load both keys from the same keypair file/source so they cannot diverge.
- Add an early guard: if (privateKeyPem ?? publicKeyPem) assert both are non-empty strings.
Example fix
// before
createFlywheelReceipt({ /* ... */, privateKeyPem });
// missing publicKeyPem
// after
createFlywheelReceipt({ /* ... */, privateKeyPem, publicKeyPem }); Defensive patterns
Strategy: type-guard
Validate before calling
if ((input.privateKeyPem ?? undefined) !== (input.publicKeyPem ?? undefined)) {
if (!input.privateKeyPem || !input.publicKeyPem) {
throw new Error('provide both privateKeyPem and publicKeyPem, or neither for an unsigned receipt');
}
} Type guard
function hasBothOrNoKeys(input: { privateKeyPem?: string; publicKeyPem?: string }): boolean {
return (!!input.privateKeyPem && !!input.publicKeyPem) || (!input.privateKeyPem && !input.publicKeyPem);
} Try / catch
try {
createFlywheelReceipt(input);
} catch (e) {
if (e instanceof Error && /both Ed25519/.test(e.message)) {
// either load the missing key or drop signing entirely
input.privateKeyPem = undefined; input.publicKeyPem = undefined;
} else throw e;
} Prevention
- Load Ed25519 keypairs from a single source so both halves are always present together.
- Treat signing as optional-by-default; only enable when both keys are confirmed.
- Add a constructor/startup check that verifies keypair completeness.
When it happens
Trigger: Calling createFlywheelReceipt({ privateKeyPem }) without publicKeyPem, or vice versa. Common when keys are loaded from separate env vars or files and one is missing.
Common situations: PEM keys split across RUFLO_FLYWHEEL_PRIVATE_KEY / RUFLO_FLYWHEEL_PUBLIC_KEY env vars where only one is set; key rotation where the new pair wasn't fully provisioned; test setup that generated one key but not the other.
Related errors
- signBacktestArtifact: privateKey must be 32 bytes (got ${pri
- signAttributionArtifact: privateKey must be 32 bytes (got ${
- Footer hash must be ${SHA256_SIZE} bytes, got ${footerHash.l
- undefined array member at ${path}[${i}]
- undefined property at ${path}.${key}
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/1f0f1d14ce2efab7.
Report an issue: GitHub.