ruvnet/ruflo · error

peer url must not embed credentials

Error message

peer url must not embed credentials

What it means

validatePeerUrl rejects URLs containing userinfo (username or password) because peer URLs may end up in logs and registry files, which would leak embedded credentials. Any parseable http/https URL with a non-empty u.username or u.password throws this error.

Solutions

  1. Remove the credentials from the URL and pass them out-of-band (headers, env vars, a secrets manager)
  2. If the endpoint requires basic auth, configure it at the HTTP client layer rather than in the peer URL
  3. Rotate any credentials that were embedded in a URL — they may already be logged
  4. Re-test with the bare origin, e.g. 'https://peer.example.com'

Example fix

// before
validatePeerUrl('https://user:secret@peer.example.com');
// after
validatePeerUrl('https://peer.example.com'); // auth supplied separately via headers/env
Defensive patterns

Strategy: validation

Validate before calling

const u = new URL(peerUrl);
if (u.username || u.password) {
  throw new Error('peer url must not embed credentials; pass them out-of-band');
}

Try / catch

try {
  const origin = validatePeerUrl(rawUrl);
} catch (e) {
  if (e.message === 'peer url must not embed credentials') {
    console.error('Credentials found in peer URL — strip them and supply auth via headers/env; rotate the leaked credential');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling validatePeerUrl or addPeer with URLs like 'https://user:pass@peer.example.com' or 'http://admin@host/' — any string where the credentials component is present, even empty-password basic auth.

Common situations: Copying a URL that included basic-auth credentials from a service dashboard or curl command; shared internal services that embed tokens in the host part; secrets pasted into config files.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


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

Appendix: source

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

function writePeers(basePath: string, peers: FederationPeer[]): void {
  ensureDir(basePath);
  writeFileSync(peersPath(basePath), JSON.stringify(peers, null, 2) + '\n');
}

/**
 * Reject anything that is not a plain http(s) URL to a host.
 *
 * 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.

View on GitHub (pinned to 2602b642d9)