{"record":{"id":"f8bdd7f84cdc0fd5","repo":"block/buzz","slug":"invalid-visibility-value-v-must-be-open-or","errorCode":null,"errorMessage":"invalid visibility value: {v} (must be \"open\" or \"private\")","messagePattern":"invalid visibility value: (.+?) \\(must be \"open\" or \"private\"\\)","errorType":"validation","errorClass":"IngestError::Rejected","httpStatus":400,"severity":"error","filePath":"crates/buzz-relay/src/handlers/side_effects.rs","lineNumber":552,"sourceCode":"                    match t.content() {\n                        Some(v)\n                            if !buzz_core::channel::canonical_channel_name(v)\n                                .trim()\n                                .is_empty() => {}\n                        _ => {\n                            return Err(anyhow::anyhow!(\"channel name is required\"));\n                        }\n                    }\n                }\n            }\n\n            // Validate visibility values before storage.\n            for t in event.tags.iter() {\n                if t.kind().to_string() == \"visibility\" {\n                    match t.content() {\n                        Some(\"open\") | Some(\"private\") => {}\n                        Some(v) => {\n                            return Err(anyhow::anyhow!(\n                                \"invalid visibility value: {v} (must be \\\"open\\\" or \\\"private\\\")\"\n                            ));\n                        }\n                        None => {\n                            return Err(anyhow::anyhow!(\"visibility tag must have a value\"));\n                        }\n                    }\n                }\n            }\n\n            // Validate ttl values before storage. Empty string clears the TTL\n            // (channel becomes permanent); any other value must parse as a\n            // positive integer number of seconds. A bare tag with no value is\n            // rejected so clearing is always explicit (`[\"ttl\", \"\"]`).\n            for t in event.tags.iter() {\n                if t.kind().to_string() == \"ttl\" {\n                    match t.content() {\n                        Some(\"\") => {}","sourceCodeStart":534,"sourceCodeEnd":570,"githubUrl":"https://github.com/block/buzz/blob/f956e6fe06a76e50cbd8fba1a162482e752e7f1a/crates/buzz-relay/src/handlers/side_effects.rs#L534-L570","documentation":"Thrown when a kind:9002 \"visibility\" tag value is not exactly \"open\" or \"private\". Like archived, the comparison is case-sensitive over the raw tag string, so \"Open\", \"PUBLIC\", \"public\", or \"unlisted\" are rejected.","triggerScenarios":"Publishing [[\"visibility\", \"Public\"], [\"visibility\", \"public\"], or [\"visibility\", \"unlisted\"]] in kind:9002. Only the two lowercase enum strings are accepted.","commonSituations":"Title-casing values for display and reusing them in the publish path; adding a third visibility tier client-side before the relay supports it; mappings from other chat systems (Slack/Discord 'private-channel' flags) converted naively.","solutions":["Use exactly \"open\" or \"private\" as the tag value.","Map domain enums at the boundary: { internal: 'PUBLIC' } → \"open\".","Validate with a strict allowlist before publish rather than a case-insensitive check."],"exampleFix":"// before\n{ kind: 9002, tags: [[\"h\", ch], [\"visibility\", isPrivate ? \"Private\" : \"OPEN\"]] }\n\n// after\n{ kind: 9002, tags: [[\"h\", ch], [\"visibility\", isPrivate ? \"private\" : \"open\"]] }","handlingStrategy":"type-guard","validationCode":"const VISIBILITIES = [\"open\", \"private\"] as const;\ntype Visibility = (typeof VISIBILITIES)[number];\n\nfunction visibilityTag(v: Visibility): string[] {\n  return [\"visibility\", v]; // type rules out \"Public\"/\"unlisted\" at compile time\n}","typeGuard":"function isVisibilityValue(v: string): v is \"open\" | \"private\" {\n  return v === \"open\" || v === \"private\";\n}","tryCatchPattern":"try {\n  await sdk.publish(editEvent);\n} catch (e) {\n  if (String(e).includes(\"invalid visibility value\")) {\n    throw new Error('Visibility must be exactly \"open\" or \"private\" (lowercase)');\n  } else throw e;\n}","preventionTips":["Model visibility as a closed union type, not a free string, at every API boundary.","Lowercase and validate external input before mapping it into event tags.","Remember there is no third tier — reject 'unlisted'/'public' client-side with a clear message."],"tags":["nostr","validation","kind-9002","enum-value","visibility","relay"],"backgroundTag":"invalid-tag-value","analyzedSha":"f956e6fe06a76e50cbd8fba1a162482e752e7f1a","analyzedAt":"2026-08-16T22:11:40.750Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}