ruvnet/ruflo · error
channel name must match [a-z0-9][a-z0-9._-]
Error message
channel name must match [a-z0-9][a-z0-9._-]{0,63} What it means
publicChannelId validates a federation channel name against CHANNEL_NAME_RE (lowercase alphanumeric start, then [a-z0-9._-], max 64 chars) and throws when the name does not match. Channel ids become pub:<name>, so the regex keeps store keys, relay topics, and URLs safe and canonical. This is a strict input-validation guard, not an environmental failure.
Solutions
- Rename the channel to lowercase kebab-case: lowercase letters/digits start, then only [a-z0-9._-], max 64 chars
- Trim whitespace and strip any 'pub:'/'prv:' prefix before passing the name
- If the desired name has uppercase or symbols, slugify it (e.g. 'Team Updates' -> 'team-updates')
Example fix
// before
publicChannelId('Team Updates'); // throws
// after
publicChannelId('team-updates');
// or slugify first
const safe = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+/, '').slice(0, 64); Defensive patterns
Strategy: validation
Validate before calling
const CHANNEL_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
function isValidChannelName(n: string): boolean { return CHANNEL_NAME_RE.test(n.trim()); } Try / catch
try {
const id = publicChannelId(name);
} catch {
const slug = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+/, '').slice(0, 64);
if (!CHANNEL_NAME_RE.test(slug)) throw new Error(`cannot slugify channel name: ${name}`);
const id = publicChannelId(slug);
} Prevention
- Always slugify user-supplied channel names to lowercase kebab-case before creating
- Strip pub:/prv: prefixes and trim whitespace before validation
- Keep names under 64 characters and starting with a letter or digit
- Validate names in UI/CLI input layers with the same regex
When it happens
Trigger: Calling any x-federation channel tool (create/join public channel) with a name containing uppercase letters, spaces, slashes, or other symbols; empty name; name longer than 64 characters; name starting with '.', '_' or '-' (must start [a-z0-9]).
Common situations: Typing a human-readable channel name like 'Team Updates' or 'My_Channel' instead of kebab-case; passing a full channel id ('pub:general') instead of the bare name; auto-generated names with timestamps containing ':' or '/'; whitespace from copy-paste.
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
- channel key must be 32 bytes (64 hex)
- pubkey must be 64 hex
- actualUsd must be a non-negative finite number
- Agent config must include id, name, and type
- agent must be an object
AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15).
Data as JSON: /api/errors/021de9f2b08aca25.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts:54
const nt = (await import('nostr-tools/pure')) as unknown as Nt;
const { nip44 } = (await import('nostr-tools')) as unknown as { nip44: Nip44 };
return { nt, nip44 };
} catch { return null; }
}
const degraded = () => ({ degraded: true, reason: 'nostr-tools not installed', hint: 'npm i nostr-tools (secp256k1 + NIP-44 are not in node:crypto)' });
/** Locally cached channel keys, 0600. Losing this file loses the channels in it — by design. */
export type ChannelStore = Record<string, { key: string; name?: string; grantedBy?: string; at: string }>;
export function readStore(file = STORE_FILE()): ChannelStore {
if (!existsSync(file)) return {};
try { return JSON.parse(readFileSync(file, 'utf8')) as ChannelStore; } catch { return {}; }
}
export function writeStore(store: ChannelStore, file = STORE_FILE()): void {
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
writeFileSync(file, JSON.stringify(store, null, 2), { mode: 0o600 });
}
export function publicChannelId(name: string): string {
if (!CHANNEL_NAME_RE.test(String(name))) throw new Error('channel name must match [a-z0-9][a-z0-9._-]{0,63}');
return `pub:${name}`;
}
export function privateChannelId(keyHex: string): string {
const k = Buffer.from(keyHex, 'hex');
if (k.length !== 32) throw new Error('channel key must be 32 bytes (64 hex)');
return `prv:${createHash('sha256').update(k).digest('hex').slice(0, 16)}`;
}
export function newChannelKey(): string { return randomBytes(32).toString('hex'); }
export function isPrivateChannel(id: string): boolean { return String(id).startsWith('prv:'); }
async function relayCall<T>(relayWs: string, sk: Uint8Array, nt: Nt, fn: (ws: WsLike) => Promise<T>): Promise<T> {
const { default: WebSocket } = await import('ws');
const ws = new WebSocket(relayWs, { perMessageDeflate: false }) as unknown as WsLike;
await new Promise<void>((resolve, reject) => {
const t = setTimeout(() => reject(new Error('relay auth timeout')), 15000);
ws.on('message', (d: Buffer) => {
const m = JSON.parse(d.toString());
if (m[0] === 'AUTH' && typeof m[1] === 'string') {View on GitHub (pinned to 2602b642d9)