Some checks failed
Go / build-and-release (push) Has been cancelled
Major refactoring of event handling into clean, testable domain services: - Add pkg/event/validation: JSON hex validation, signature verification, timestamp bounds, NIP-70 protected tag validation - Add pkg/event/authorization: Policy and ACL authorization decisions, auth challenge handling, access level determination - Add pkg/event/routing: Event router registry with ephemeral and delete handlers, kind-based dispatch - Add pkg/event/processing: Event persistence, delivery to subscribers, and post-save hooks (ACL reconfig, sync, relay groups) - Reduce handle-event.go from 783 to 296 lines (62% reduction) - Add comprehensive unit tests for all new domain services - Refactor database tests to use shared TestMain setup - Fix blossom URL test expectations (missing "/" separator) - Add go-memory-optimization skill and analysis documentation - Update DDD_ANALYSIS.md to reflect completed decomposition Files modified: - app/handle-event.go: Slim orchestrator using domain services - app/server.go: Service initialization and interface wrappers - app/handle-event-types.go: Shared types (OkHelper, result types) - pkg/event/validation/*: New validation service package - pkg/event/authorization/*: New authorization service package - pkg/event/routing/*: New routing service package - pkg/event/processing/*: New processing service package - pkg/database/*_test.go: Refactored to shared TestMain - pkg/blossom/http_test.go: Fixed URL format expectations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
165 lines
4.4 KiB
Go
165 lines
4.4 KiB
Go
package validation
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
)
|
|
|
|
// ValidateLowercaseHexInJSON checks that all hex-encoded fields in the raw JSON are lowercase.
|
|
// NIP-01 specifies that hex encoding must be lowercase.
|
|
// This must be called on the raw message BEFORE unmarshaling, since unmarshal converts
|
|
// hex strings to binary and loses case information.
|
|
// Returns an error message if validation fails, or empty string if valid.
|
|
func ValidateLowercaseHexInJSON(msg []byte) string {
|
|
// Find and validate "id" field (64 hex chars)
|
|
if err := validateJSONHexField(msg, `"id"`); err != "" {
|
|
return err + " (id)"
|
|
}
|
|
|
|
// Find and validate "pubkey" field (64 hex chars)
|
|
if err := validateJSONHexField(msg, `"pubkey"`); err != "" {
|
|
return err + " (pubkey)"
|
|
}
|
|
|
|
// Find and validate "sig" field (128 hex chars)
|
|
if err := validateJSONHexField(msg, `"sig"`); err != "" {
|
|
return err + " (sig)"
|
|
}
|
|
|
|
// Validate e and p tags in the tags array
|
|
// Tags format: ["e", "hexvalue", ...] or ["p", "hexvalue", ...]
|
|
if err := validateEPTagsInJSON(msg); err != "" {
|
|
return err
|
|
}
|
|
|
|
return "" // Valid
|
|
}
|
|
|
|
// validateJSONHexField finds a JSON field and checks if its hex value contains uppercase.
|
|
func validateJSONHexField(msg []byte, fieldName string) string {
|
|
// Find the field name
|
|
idx := bytes.Index(msg, []byte(fieldName))
|
|
if idx == -1 {
|
|
return "" // Field not found, skip
|
|
}
|
|
|
|
// Find the colon after the field name
|
|
colonIdx := bytes.Index(msg[idx:], []byte(":"))
|
|
if colonIdx == -1 {
|
|
return ""
|
|
}
|
|
|
|
// Find the opening quote of the value
|
|
valueStart := idx + colonIdx + 1
|
|
for valueStart < len(msg) && (msg[valueStart] == ' ' || msg[valueStart] == '\t' || msg[valueStart] == '\n' || msg[valueStart] == '\r') {
|
|
valueStart++
|
|
}
|
|
if valueStart >= len(msg) || msg[valueStart] != '"' {
|
|
return ""
|
|
}
|
|
valueStart++ // Skip the opening quote
|
|
|
|
// Find the closing quote
|
|
valueEnd := valueStart
|
|
for valueEnd < len(msg) && msg[valueEnd] != '"' {
|
|
valueEnd++
|
|
}
|
|
|
|
// Extract the hex value and check for uppercase
|
|
hexValue := msg[valueStart:valueEnd]
|
|
if containsUppercaseHex(hexValue) {
|
|
return "blocked: hex fields may only be lower case, see NIP-01"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// validateEPTagsInJSON checks e and p tags in the JSON for uppercase hex.
|
|
func validateEPTagsInJSON(msg []byte) string {
|
|
// Find the tags array
|
|
tagsIdx := bytes.Index(msg, []byte(`"tags"`))
|
|
if tagsIdx == -1 {
|
|
return "" // No tags
|
|
}
|
|
|
|
// Find the opening bracket of the tags array
|
|
bracketIdx := bytes.Index(msg[tagsIdx:], []byte("["))
|
|
if bracketIdx == -1 {
|
|
return ""
|
|
}
|
|
|
|
tagsStart := tagsIdx + bracketIdx
|
|
|
|
// Scan through to find ["e", ...] and ["p", ...] patterns
|
|
// This is a simplified parser that looks for specific patterns
|
|
pos := tagsStart
|
|
for pos < len(msg) {
|
|
// Look for ["e" or ["p" pattern
|
|
eTagPattern := bytes.Index(msg[pos:], []byte(`["e"`))
|
|
pTagPattern := bytes.Index(msg[pos:], []byte(`["p"`))
|
|
|
|
var tagType string
|
|
var nextIdx int
|
|
|
|
if eTagPattern == -1 && pTagPattern == -1 {
|
|
break // No more e or p tags
|
|
} else if eTagPattern == -1 {
|
|
nextIdx = pos + pTagPattern
|
|
tagType = "p"
|
|
} else if pTagPattern == -1 {
|
|
nextIdx = pos + eTagPattern
|
|
tagType = "e"
|
|
} else if eTagPattern < pTagPattern {
|
|
nextIdx = pos + eTagPattern
|
|
tagType = "e"
|
|
} else {
|
|
nextIdx = pos + pTagPattern
|
|
tagType = "p"
|
|
}
|
|
|
|
// Find the hex value after the tag type
|
|
// Pattern: ["e", "hexvalue" or ["p", "hexvalue"
|
|
commaIdx := bytes.Index(msg[nextIdx:], []byte(","))
|
|
if commaIdx == -1 {
|
|
pos = nextIdx + 4
|
|
continue
|
|
}
|
|
|
|
// Find the opening quote of the hex value
|
|
valueStart := nextIdx + commaIdx + 1
|
|
for valueStart < len(msg) && (msg[valueStart] == ' ' || msg[valueStart] == '\t' || msg[valueStart] == '"') {
|
|
if msg[valueStart] == '"' {
|
|
valueStart++
|
|
break
|
|
}
|
|
valueStart++
|
|
}
|
|
|
|
// Find the closing quote
|
|
valueEnd := valueStart
|
|
for valueEnd < len(msg) && msg[valueEnd] != '"' {
|
|
valueEnd++
|
|
}
|
|
|
|
// Check if this looks like a hex value (64 chars for pubkey/event ID)
|
|
hexValue := msg[valueStart:valueEnd]
|
|
if len(hexValue) == 64 && containsUppercaseHex(hexValue) {
|
|
return fmt.Sprintf("blocked: hex fields may only be lower case, see NIP-01 (%s tag)", tagType)
|
|
}
|
|
|
|
pos = valueEnd + 1
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// containsUppercaseHex checks if a byte slice (representing hex) contains uppercase letters A-F.
|
|
func containsUppercaseHex(b []byte) bool {
|
|
for _, c := range b {
|
|
if c >= 'A' && c <= 'F' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|