ruvnet/ruflo · error

publicKey must be 64 lowercase hex chars

Error message

publicKey must be 64 lowercase hex chars

What it means

addPeer() validates that a peer's Ed25519-style public key is exactly 64 lowercase hex characters before writing it to the federation peer registry. The key is pinned for TOFU (trust-on-first-use) identity verification, so any malformed key is rejected up front. The library throws this error instead of accepting a key it could later use to authenticate peers.

Solutions

  1. Re-encode the peer's public key as 64 lowercase hex characters (e.g. hex of the 32-byte key) before calling addPeer.
  2. Trim whitespace/newlines from the key string: publicKey.trim().toLowerCase().
  3. Verify the key length with publicKey.length === 64 and /H{64}/.test(publicKey); if using a base64 key, convert with Buffer.from(key,'base64').toString('hex').
  4. Regenerate/export the peer's identity key in hex format if the source format is not convertible.

Example fix

// before
addPeer(base, { nodeId, url, publicKey: 'AbC123...' });
// after
const pk = rawKey.trim().toLowerCase();
if (!/^[0-9a-f]{64}$/.test(pk)) throw new Error('bad key');
addPeer(base, { nodeId, url, publicKey: pk });
Defensive patterns

Strategy: validation

Validate before calling

function isValidPublicKey(k) { return typeof k === 'string' && /^[0-9a-f]{64}$/.test(k.trim()); }
if (!isValidPublicKey(input.publicKey)) throw new Error('peer publicKey must be 64 lowercase hex chars');

Type guard

const isHex64 = (v: unknown): v is string => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v);

Prevention

When it happens

Trigger: Calling addPeer(basePath, { nodeId, url, publicKey }) where publicKey is not 64 lowercase hex chars: uppercase hex, 32/33-char base58/NaCl key pasted by mistake, key with whitespace/newline, base64-encoded key, or a truncated string.

Common situations: Copying a public key from an SSH key or terminal output that wrapped/truncated it; mixing key formats between node versions (e.g. base64 in v1, hex in v2); a config file containing a placeholder like 'REPLACE_ME'; shell quoting stripping or adding characters.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15). Data as JSON: /api/errors/644d8130ea77ff7a. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-tools/agentbbs-federation.ts:265

 * Blocks credentials-in-URL (they would be logged), and non-http schemes such
 * as `file:` which would turn a peer entry into a local file read.
 */
export function validatePeerUrl(raw: string): string {
  let u: URL;
  try { u = new URL(raw); } catch { throw new Error('peer url is not a valid URL'); }
  if (u.protocol !== 'http:' && u.protocol !== 'https:') {
    throw new Error('peer url must be http or https');
  }
  if (u.username || u.password) throw new Error('peer url must not embed credentials');
  return u.origin;
}

export function addPeer(
  basePath: string,
  input: { nodeId: string; url: string; publicKey: string; label?: string },
): FederationPeer {
  if (!NODE_ID_RE.test(input.nodeId ?? '')) throw new Error('nodeId must be 16 lowercase hex chars');
  if (!HEX64_RE.test(input.publicKey ?? '')) throw new Error('publicKey must be 64 lowercase hex chars');
  const url = validatePeerUrl(String(input.url));

  const peers = readPeers(basePath);
  if (peers.length >= MAX_PEERS) throw new Error(`peer registry is full (${MAX_PEERS})`);

  const existing = peers.find(p => p.nodeId === input.nodeId);
  if (existing) {
    // Re-pinning a different key for a known nodeId is how a key-substitution
    // attack would present. Require an explicit remove first.
    if (existing.publicKey !== input.publicKey) {
      throw new Error(`nodeId ${input.nodeId} is already pinned to a different publicKey; remove it first`);
    }
    existing.url = url;
    if (input.label) existing.label = input.label;
    writePeers(basePath, peers);
    return existing;
  }

View on GitHub (pinned to 2602b642d9)