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

  1. Pass BOTH privateKeyPem and publicKeyPem, or pass NEITHER for an unsigned receipt.
  2. Load both keys from the same keypair file/source so they cannot diverge.
  3. 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

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


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/1f0f1d14ce2efab7. Report an issue: GitHub.