Files
next.orly.dev/pkg/event/validation/hex.go
mleku 24383ef1f4
Some checks failed
Go / build-and-release (push) Has been cancelled
Decompose handle-event.go into DDD domain services (v0.36.15)
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>
2025-12-25 05:30:07 +01:00

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
}