ruvnet/ruflo · error
nodeId must be 16 lowercase hex chars
Error message
nodeId must be 16 lowercase hex chars
What it means
addPeer validates every new peer's nodeId against NODE_ID_RE (16 lowercase hex chars) before writing it to the peer registry. This error means the supplied nodeId string is missing, mistyped, or formatted outside the required canonical identifier format, preventing registry entries keyed by untrusted identifiers.
Solutions
- Obtain the peer's actual nodeId (e.g. from the peer's getNodeIdentity output) and pass that exact string
- Ensure it is exactly 16 lowercase hex characters matching /^[0-9a-f]{16}$/ — lowercase it and strip prefixes like '0x' or whitespace before calling
- Check that the nodeId field is actually populated in your config/env source (not undefined)
- If you generated the peer yourself, regenerate or read the identity file to recover its canonical nodeId
Example fix
// before
addPeer(base, { nodeId: '0A1B-2C3D', url: 'https://p.example.com', publicKey: PK });
// after
addPeer(base, { nodeId: '0a1b2c3d4e5f6071', url: 'https://p.example.com', publicKey: PK }); Defensive patterns
Strategy: validation
Validate before calling
const NODE_ID_RE = /^[0-9a-f]{16}$/;
if (!NODE_ID_RE.test(nodeId)) {
throw new Error(`invalid nodeId '${nodeId}': must be exactly 16 lowercase hex chars`);
} Type guard
function isNodeId(v: unknown): v is string {
return typeof v === 'string' && /^[0-9a-f]{16}$/.test(v);
} Try / catch
try {
const peer = addPeer(basePath, input);
} catch (e) {
if (e.message === 'nodeId must be 16 lowercase hex chars') {
console.error(`Peer '${input.label ?? input.nodeId}' has an invalid nodeId; fetch the peer's real nodeId via getNodeIdentity`);
} else throw e;
} Prevention
- Copy nodeIds directly from the peer's identity file, not from logs or labels
- Normalize before use: strip '0x', whitespace, separators, and lowercase the string
- Validate config-supplied nodeIds with /^[0-9a-f]{16}$/ at startup
- Never reuse a label or hostname as a nodeId
When it happens
Trigger: Calling addPeer(basePath, { nodeId: <value>, url, publicKey }) where input.nodeId fails NODE_ID_RE.test — e.g. undefined/null, uppercase hex, 15 or 17 chars, containing '-', '0x' prefixes, or non-hex characters.
Common situations: Passing the peer's human-readable name or label instead of its nodeId; copying an uppercased or decorated ID from logs; truncating/pasting the ID incorrectly; reading nodeId from an env var that is unset.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- channel must be pub: or prv:<16 hex>
- invalid
- invite code must look like v2.
- label may only contain [A-Za-z0-9_.\\-:/@]
- msgType must be alnum + _ - and ≤64 chars
AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15).
Data as JSON: /api/errors/ca98d125bcb9de51.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/agentbbs-federation.ts:264
*
* 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)