{"record":{"id":"e93261ce4f94d938","repo":"block/buzz","slug":"target-event-belongs-to-a-different-channel","errorCode":null,"errorMessage":"target event belongs to a different channel","messagePattern":"target event belongs to a different channel","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":661,"sourceCode":"                        tag.content().and_then(|v| hex::decode(v).ok())\n                    } else {\n                        None\n                    }\n                })\n                .ok_or_else(|| anyhow::anyhow!(\"missing e tag for target event\"))?;\n\n            // Verify the target event exists and belongs to the h-tag channel\n            // BEFORE storage. Fail closed: missing target → reject.\n            let target_event = state\n                .db\n                .get_event_by_id(tenant.community(), &target_id)\n                .await\n                .map_err(|e| anyhow::anyhow!(\"db error looking up target: {e}\"))?\n                .ok_or_else(|| anyhow::anyhow!(\"target event not found\"))?;\n\n            match target_event.channel_id {\n                Some(target_ch) if target_ch != channel_id => {\n                    return Err(anyhow::anyhow!(\n                        \"target event belongs to a different channel\"\n                    ));\n                }\n                None => {\n                    return Err(anyhow::anyhow!(\"target event has no channel\"));\n                }\n                _ => {} // Same channel — OK\n            }\n\n            // Check if actor is the event author.\n            // For relay-signed REST messages, the real author is in the p tag.\n            let author =\n                effective_message_author(&target_event.event, &state.relay_keypair.public_key());\n            if author_delete_can_use_self_delete_path(&author, &actor_bytes, event) {\n                // Author deleting their own message: re-gate on membership/open visibility so that\n                // a removed private-channel member cannot mutate old messages after access is revoked.\n                let is_member = state\n                    .is_member_cached(tenant.community(), channel_id, &actor_bytes)","sourceCodeStart":643,"sourceCodeEnd":679,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L643-L679","documentation":"Thrown by kind:9005 when the target event exists but its stored channel_id differs from the channel in the delete event's h tag. Deletes are channel-scoped: the relay looks up the target, then requires target_event.channel_id == the h-tag channel before checking authorship. This prevents cross-channel deletion via forged h tags.","triggerScenarios":"Publishing kind:9005 with h tag of channel A but an e tag pointing at a message in channel B. Commonly happens with stale cached channel ids or when a message was moved/cross-posted and the client tracks the wrong channel.","commonSituations":"Deep links or search results yielding a message id that the client associates with the wrong channel; channel deleted and recreated with the same name but different uuid; copy-paste of e ids between channels in tooling.","solutions":["Resolve the target message's actual channel first (query the event by id and read its h tag / channel), then publish the 9005 with THAT channel in the h tag.","In UI flows, always pair the delete with the channel the message was rendered from, not a global channel id.","Refresh cached channel ids (buzz channels list) if a channel was recreated."],"exampleFix":"// before — deleting with the wrong h tag\nawait sdk.deleteEvent(wrongChannelId, targetEventId); // target lives in otherChannelId\n\n// after — use the target's own channel\nconst target = await buzz.messagesGet(targetEventId);\nawait sdk.deleteEvent(target.channelId, targetEventId);","handlingStrategy":"validation","validationCode":"// Resolve the target's own channel before publishing the delete\nconst target = await buzz.messagesGet(targetEventId);\nconst hTag = target.tags.find((t) => t[0] === \"h\");\nif (!hTag) throw new Error(\"Target is not a channel message\");\nif (hTag[1] !== currentChannelId) {\n  currentChannelId = hTag[1]; // use the target's actual channel\n}\nawait sdk.deleteEvent(currentChannelId, targetEventId);","typeGuard":"function targetMatchesChannel(targetHTag: string | undefined, deleteHTag: string): boolean {\n  return targetHTag === deleteHTag;\n}","tryCatchPattern":"try {\n  await sdk.deleteEvent(channelId, eventId);\n} catch (e) {\n  if (String(e).includes(\"belongs to a different channel\")) {\n    const target = await buzz.messagesGet(eventId);\n    await sdk.deleteEvent(target.tags.find((t) => t[0] === \"h\")![1], eventId);\n  } else throw e;\n}","preventionTips":["Always pair a delete with the channel the message was actually received in, not the currently-viewed channel.","Refresh channel ids after channels are deleted/recreated — names persist, uuids change.","In search/deep-link flows, carry the channel id alongside the event id."],"tags":["nostr","validation","kind-9005","channel-mismatch","relay"],"backgroundTag":"channel-mismatch","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}