ruvnet/ruflo · error

pubkey must be 64 hex

Error message

pubkey must be 64 hex

What it means

The grant-key handler validates the member's Nostr pubkey against /^[0-9a-f]{64}$/i before deriving the NIP-44 conversation key; Nostr pubkeys are 32-byte x-only keys serialized as 64 hex characters. It throws when the pubkey is malformed, the wrong length, or in another encoding (bech32/nprofile, base64).

Solutions

  1. Use the member's raw 64-char hex pubkey (x-only, no prefix); convert npub1... to hex with a bech32 decoder (e.g. nip19 decode) first
  2. Trim whitespace and strip 0x prefixes; verify with /^[0-9a-f]{64}$/i before calling
  3. If only an npub is available, decode it: nip19.npubDecode(npub) -> hex pubkey

Example fix

// before
grant({ channel: 'prv:1a2b...', pubkey: 'npub1sg6plzptd64u6a8aa...' }); // throws
// after
const hex = nip19.npubDecode('npub1sg6plzptd64u6a8aa...'); // 64 hex chars
grant({ channel: 'prv:1a2b...', pubkey: hex });
Defensive patterns

Strategy: validation

Validate before calling

function isValidNostrPubkey(pk: string): boolean {
  return /^[0-9a-f]{64}$/i.test(String(pk).trim());
}

Type guard

function isHexPubkey(x: unknown): x is string {
  return typeof x === 'string' && /^[0-9a-f]{64}$/i.test(x);
}

Try / catch

try {
  await grantKey({ channel, pubkey });
} catch (e) {
  if (e.message === 'pubkey must be 64 hex') {
    const hex = npubToHex(pubkey); // decode bech32 npub if needed
    if (isHexPubkey(hex)) return grantKey({ channel, pubkey: hex.trim() });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling the grant-key tool with a pubkey that is shorter/longer than 64 hex chars, contains non-hex characters, has an npub/nprofile prefix, includes whitespace, or is empty.

Common situations: Pasting an npub1... bech32 address instead of the raw hex pubkey; truncating the pubkey during copy-paste; uppercase-hex is fine (i flag) but adding a 0x prefix is not; confusing the member pubkey with the local identity key.

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


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

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-channels.ts:135

      if (visibility === 'public') return { channel: publicChannelId(name), visibility, note: 'Any relay member can read this channel.' };
      const key = newChannelKey(); const channel = privateChannelId(key);
      const store = readStore(); store[channel] = { key, name, at: new Date().toISOString() }; writeStore(store);
      return { channel, visibility, name, keyStoredAt: STORE_FILE(),
        note: 'The key never leaves this machine. Grant others with x_federation_channel_grant. There is no recovery if the key file is lost, and no revocation — removing someone means rotating to a new channel.' };
    },
  },
  {
    name: 'x_federation_channel_grant',
    description: "Grant a member access to a private channel by sealing its key to their pubkey with NIP-44 (ECDH), published as a ChannelGrant event only they can open. Use when adding a participant to an existing private channel. Publishing the raw key into a channel or a chat is wrong: it is a bearer secret, and anyone who sees it can read every past and future message, because there is no revocation.",
    inputSchema: { type: 'object', properties: {
      channel: { type: 'string', description: 'Private channel id (prv:<16 hex>) you hold the key for.' },
      pubkey: { type: 'string', description: "The member's 64-hex Nostr pubkey." },
      relayWs: { type: 'string', description: 'Relay URL; takes precedence over RUFLO_X_RELAY_WS (default wss://relay.ruv.io).' },
    }, required: ['channel', 'pubkey'] },
    handler: async (input) => {
      const i = input as { channel: string; pubkey: string; relayWs?: string };
      if (!isPrivateChannel(i.channel)) throw new Error('only private channels have keys to grant');
      if (!/^[0-9a-f]{64}$/i.test(i.pubkey)) throw new Error('pubkey must be 64 hex');
      const t = await loadTools(); if (!t) return degraded();
      const entry = readStore()[i.channel];
      if (!entry) throw new Error(`no key held for ${i.channel} — create it or accept a grant first`);
      const { sk, pubkey } = loadOrCreateKey(t.nt as never, KEY_FILE());
      const conv = t.nip44.v2.utils.getConversationKey(sk, i.pubkey);
      const sealed = t.nip44.v2.encrypt(entry.key, conv);
      const relay = RELAY_WS(i.relayWs);
      const eventId = await relayCall(relay, sk, t.nt, (ws) => publishEvent(ws, t.nt, sk,
        [['t', 'ruflo-swarm'], ['k', 'ChannelGrant'], ['c', i.channel], ['p', i.pubkey]],
        JSON.stringify({ type: 'ChannelGrant', channel: i.channel, sealed, ts: new Date().toISOString() })));
      return { ok: true, channel: i.channel, grantedTo: i.pubkey, grantedBy: pubkey, eventId,
        note: 'Only that pubkey can open the seal. Grants are not revocable — rotate the channel to remove someone.' };
    },
  },
  {
    name: 'x_federation_channel_accept',
    description: 'Accept private-channel grants addressed to your key: finds ChannelGrant events tagged to your pubkey, opens each with your own secret key, and caches the channel keys locally. Use when someone tells you they granted you a channel. Asking them to send you the key directly is wrong because it exposes a bearer secret in a channel you do not control.',
    inputSchema: { type: 'object', properties: {

View on GitHub (pinned to 2602b642d9)