{"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":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":605,"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_for_event_write(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":587,"sourceCodeEnd":623,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-relay/src/handlers/side_effects.rs#L587-L623","documentation":"The kind:9005 delete handler found the e-tag target but its stored channel_id is NULL, meaning it is not a channel message — typically an addressable/control event such as kind:39000 metadata, a membership list, a DM wrapper, or other non-channel state. The message-delete path only applies to channel-scoped events, so the relay rejects it instead of guessing a channel from the h tag.","triggerScenarios":"Pointing a 9005 delete at a kind:39000/39001/39002 channel-definition event, a user-metadata event, or any event stored without channel assignment; using the generic delete flow where a channel-specific admin operation is required.","commonSituations":"Tooling that 'deletes anything by id' hitting control events; debugging with a message id that is actually the channel's d-tag addressable event.","solutions":["Use the target's kind to route: channel messages (e.g., kind 40002 and other message kinds) go through 9005; addressable channel state uses its own admin/replace flow (republish via reconcile-channels or the owning kind).","Check what the id actually is before deleting: relay query with { ids: [id] } and inspect kind and tags.","If the target is channel metadata that must change, publish a replacement event of the same kind/d tag instead of a 9005 delete.","Fix client code that reuses one delete routine for all event types."],"exampleFix":"// before: one delete path for everything\nawait publish(delete9005(targetId, currentChannel)); // fails when target is kind:39000\n// after: route by target kind\nconst target = await relay.queryOne({ ids: [targetId] });\nif (target.tags.some(([k]) => k === 'h')) {\n  await publish(delete9005(targetId, channelOf(target)));\n} else {\n  await publish(replaceAddressable(target.kind, dTagOf(target), newContent)); // or admin op\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// only channel-scoped events may enter the 9005 path: they carry an h tag\nfunction isChannelMessage(ev: NostrEvent): boolean {\n  return ev.tags.some(([k]) => k === 'h');\n}","tryCatchPattern":null,"preventionTips":["Route deletes by target kind: h-tagged messages -> 9005; addressable state -> republish the same kind/d tag.","Inspect unknown ids with a { ids: [id] } query before choosing the operation.","Keep a shared 'message kinds' constant list so generic delete helpers only accept message kinds."],"tags":["buzz-relay","nostr","delete-event","channel-scoping","event-kinds"],"backgroundTag":"invalid-event-target","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-08-20T04:38:24.874Z","contentChangedAt":"2026-08-20T04:38:24.874Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}