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.. // // 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..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..state. func (p *Publisher) PublishState(ctx context.Context, tournamentID, stateType string, data []byte) (*jetstream.PubAck, error) { return p.Publish(ctx, tournamentID, "state."+stateType, data) }