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
- Re-encode the peer's public key as 64 lowercase hex characters (e.g. hex of the 32-byte key) before calling addPeer.
- Trim whitespace/newlines from the key string: publicKey.trim().toLowerCase().
- 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').
- 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
- Normalize keys with .trim().toLowerCase() at config load time
- Keep keys in config files without line wrapping or shell interpolation
- Store and transport keys only in hex between services
- Add a startup assertion validating every configured peer key
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
- Invalid item ID
- Invalid worker type
- Rating must be integer 1-5
- roomId exceeds 128 chars
- roomId is required
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)