{"record":{"id":"33ff9f58bd442161","repo":"block/buzz","slug":"target-event-has-no-channel","errorCode":null,"errorMessage":"target event has no channel","messagePattern":"target event has no channel","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":666,"sourceCode":"                .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)\n                    .await?;\n                if is_member {\n                    return Ok(());\n                }\n                let is_open = state","sourceCodeStart":648,"sourceCodeEnd":684,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L648-L684","documentation":"Thrown by kind:9005 when the target event exists but has channel_id = NULL in the database — it is not a channel message. The delete-by-event path only operates on channel-scoped events; direct messages, DM wrappers, and other non-channel events cannot be deleted this way.","triggerScenarios":"Pointing a kind:9005 e tag at a kind:14 DM/gift-wrap, a user-metadata event, or any event stored without a channel association. The h-tag scoping check finds None and rejects.","commonSituations":"Generic 'delete any message' UI actions that feed whatever event id is selected into the channel delete path; scripts iterating an inbox containing DMs; deletes targeting relay-signed REST artifacts recorded without channel scope.","solutions":["Only send kind:9005 for events that actually belong to a channel (they were received via a channel subscription / have an h tag).","For DM deletion, use the DM-specific mechanism (kind:5 deletion event referencing the target e tag) rather than 9005.","Filter candidate ids client-side: skip events whose channel association is unknown.","If you expected the event to be in a channel, verify how it was published — it may have been stored outside channel scope by design."],"exampleFix":"// before — universal delete path feeds DM ids into 9005\nawait sdk.deleteEvent(currentChannelId, selectedEventId); // selectedEventId is a DM → \"target event has no channel\"\n\n// after — branch on event scope\nif (event.tags.some((t) => t[0] === \"h\")) {\n  await sdk.deleteEvent(channelId, event.id);          // channel message → 9005\n} else {\n  await sdk.publishDeletion(event.id);                 // kind:5 generic deletion\n}","handlingStrategy":"type-guard","validationCode":"// Only route channel-scoped events into the kind:9005 path\nconst target = await buzz.messagesGet(eventId);\nif (!target.tags.some((t) => t[0] === \"h\")) {\n  // DM or non-channel event — use the generic deletion kind instead\n  await sdk.publishDeletion(eventId);\n} else {\n  await sdk.deleteEvent(channelId, eventId);\n}","typeGuard":"function isChannelEvent(tags: string[][]): boolean {\n  return tags.some((t) => t[0] === \"h\" && typeof t[1] === \"string\" && t[1].length > 0);\n}","tryCatchPattern":"try {\n  await sdk.deleteEvent(channelId, eventId);\n} catch (e) {\n  if (String(e).includes(\"target event has no channel\")) {\n    await sdk.publishDeletion(eventId); // fall back to generic kind:5 deletion\n  } else throw e;\n}","preventionTips":["Branch delete flows on event scope: channel messages via 9005, DMs via their own deletion mechanism.","Do not feed ids from unified inboxes (which mix DMs and channel messages) into the channel delete path.","When publishing events you may later delete, keep the h tag so they stay channel-scoped."],"tags":["nostr","validation","kind-9005","non-channel-event","relay"],"backgroundTag":"unsupported-target-event","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}