- Embedded NATS server with JetStream (sync_interval=always per Jepsen 2025) - AUDIT and STATE JetStream streams for tournament event durability - NATS publisher with UUID validation to prevent subject injection - WebSocket hub with JWT auth (query param), tournament-scoped broadcasting - Origin validation and slow-consumer message dropping - chi HTTP router with middleware (logger, recoverer, request ID, CORS, body limits) - Server timeouts: ReadHeader 10s, Read 30s, Write 60s, Idle 120s, MaxHeader 1MB - MaxBytesReader middleware for request body limits (1MB default) - JWT auth middleware with HMAC-SHA256 validation - Role-based access control (admin > floor > viewer) - Health endpoint reporting all subsystem status (DB, NATS, WebSocket) - SvelteKit SPA served via go:embed with fallback routing - Signal-driven graceful shutdown in reverse startup order - 9 integration tests covering all verification criteria Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
77 lines
2.4 KiB
Go
77 lines
2.4 KiB
Go
package nats
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// uuidRegex validates UUID format (8-4-4-4-12 hex digits).
|
|
var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
|
|
|
// Publisher provides tournament-scoped event publishing to JetStream.
|
|
type Publisher struct {
|
|
js jetstream.JetStream
|
|
}
|
|
|
|
// NewPublisher creates a new publisher using the given JetStream context.
|
|
func NewPublisher(js jetstream.JetStream) *Publisher {
|
|
return &Publisher{js: js}
|
|
}
|
|
|
|
// ValidateUUID checks that the given string is a valid UUID and does not
|
|
// contain NATS subject wildcards (* > .) that could enable subject injection.
|
|
func ValidateUUID(id string) error {
|
|
if id == "" {
|
|
return fmt.Errorf("empty UUID")
|
|
}
|
|
|
|
// Check for NATS subject wildcards that could cause injection
|
|
if strings.ContainsAny(id, "*>.") {
|
|
return fmt.Errorf("UUID contains NATS subject wildcards: %q", id)
|
|
}
|
|
|
|
if !uuidRegex.MatchString(id) {
|
|
return fmt.Errorf("invalid UUID format: %q", id)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Publish publishes a tournament-scoped event to JetStream.
|
|
// The full subject is constructed as: tournament.<tournamentID>.<subject>
|
|
//
|
|
// The tournamentID is validated as a proper UUID before subject construction
|
|
// to prevent subject injection attacks.
|
|
func (p *Publisher) Publish(ctx context.Context, tournamentID, subject string, data []byte) (*jetstream.PubAck, error) {
|
|
if err := ValidateUUID(tournamentID); err != nil {
|
|
return nil, fmt.Errorf("publish: invalid tournament ID: %w", err)
|
|
}
|
|
|
|
if subject == "" {
|
|
return nil, fmt.Errorf("publish: empty subject")
|
|
}
|
|
|
|
fullSubject := fmt.Sprintf("tournament.%s.%s", tournamentID, subject)
|
|
ack, err := p.js.Publish(ctx, fullSubject, data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("publish to %s: %w", fullSubject, err)
|
|
}
|
|
|
|
return ack, nil
|
|
}
|
|
|
|
// PublishAudit is a convenience method for publishing audit events.
|
|
// Subject: tournament.<tournamentID>.audit
|
|
func (p *Publisher) PublishAudit(ctx context.Context, tournamentID string, data []byte) (*jetstream.PubAck, error) {
|
|
return p.Publish(ctx, tournamentID, "audit", data)
|
|
}
|
|
|
|
// PublishState is a convenience method for publishing state change events.
|
|
// Subject: tournament.<tournamentID>.state.<stateType>
|
|
func (p *Publisher) PublishState(ctx context.Context, tournamentID, stateType string, data []byte) (*jetstream.PubAck, error) {
|
|
return p.Publish(ctx, tournamentID, "state."+stateType, data)
|
|
}
|