// Package idgen provides the default crypto/rand-backed implementation of // ports.IDGenerator for Game Lobby Service. package idgen import ( "crypto/rand" "encoding/base32" "fmt" "io" "strings" "galaxy/lobby/internal/domain/common" ) // gameIDTokenBytes stores the number of random bytes consumed per // NewGameID call. Ten bytes produce a 16-character base32 suffix, which // gives 80 bits of entropy — well above the birthday-collision bound for the // expected Game Lobby record volume. const gameIDTokenBytes = 10 // applicationIDTokenBytes mirrors gameIDTokenBytes for application records. // 80 bits of entropy is well above the birthday-collision bound for the // expected application volume. const applicationIDTokenBytes = 10 // inviteIDTokenBytes mirrors gameIDTokenBytes for invite records. const inviteIDTokenBytes = 10 // membershipIDTokenBytes mirrors gameIDTokenBytes for membership records. const membershipIDTokenBytes = 10 // base32NoPadding is the standard RFC 4648 base32 alphabet without padding, // matching the identifier shape used by `galaxy/user/internal/adapters/local`. var base32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) // Generator is the default opaque-identifier generator for Game Lobby // records. Zero value is ready for use and draws randomness from // crypto/rand.Reader. type Generator struct { // reader stores the cryptographic randomness source. A nil reader falls // back to crypto/rand.Reader. reader io.Reader } // Option configures an optional Generator setting. type Option func(*Generator) // WithRandomSource overrides the cryptographic randomness source. It is // intended for deterministic tests; production code relies on the default // crypto/rand.Reader. func WithRandomSource(reader io.Reader) Option { return func(gen *Generator) { gen.reader = reader } } // NewGenerator constructs one Generator with the supplied options applied. func NewGenerator(opts ...Option) *Generator { gen := &Generator{} for _, opt := range opts { opt(gen) } return gen } // NewGameID returns one newly generated opaque game identifier with the // frozen `game-*` prefix. func (gen *Generator) NewGameID() (common.GameID, error) { token, err := gen.randomToken(gameIDTokenBytes) if err != nil { return "", fmt.Errorf("generate game id: %w", err) } gameID := common.GameID("game-" + token) if err := gameID.Validate(); err != nil { return "", fmt.Errorf("generate game id: %w", err) } return gameID, nil } // NewApplicationID returns one newly generated opaque application // identifier with the frozen `application-*` prefix. func (gen *Generator) NewApplicationID() (common.ApplicationID, error) { token, err := gen.randomToken(applicationIDTokenBytes) if err != nil { return "", fmt.Errorf("generate application id: %w", err) } applicationID := common.ApplicationID("application-" + token) if err := applicationID.Validate(); err != nil { return "", fmt.Errorf("generate application id: %w", err) } return applicationID, nil } // NewInviteID returns one newly generated opaque invite identifier with the // frozen `invite-*` prefix. func (gen *Generator) NewInviteID() (common.InviteID, error) { token, err := gen.randomToken(inviteIDTokenBytes) if err != nil { return "", fmt.Errorf("generate invite id: %w", err) } inviteID := common.InviteID("invite-" + token) if err := inviteID.Validate(); err != nil { return "", fmt.Errorf("generate invite id: %w", err) } return inviteID, nil } // NewMembershipID returns one newly generated opaque membership identifier // with the frozen `membership-*` prefix. func (gen *Generator) NewMembershipID() (common.MembershipID, error) { token, err := gen.randomToken(membershipIDTokenBytes) if err != nil { return "", fmt.Errorf("generate membership id: %w", err) } membershipID := common.MembershipID("membership-" + token) if err := membershipID.Validate(); err != nil { return "", fmt.Errorf("generate membership id: %w", err) } return membershipID, nil } // randomToken returns one lowercase base32 token of the specified byte // entropy. func (gen *Generator) randomToken(byteCount int) (string, error) { buffer := make([]byte, byteCount) reader := gen.reader if reader == nil { reader = rand.Reader } if _, err := io.ReadFull(reader, buffer); err != nil { return "", err } return strings.ToLower(base32NoPadding.EncodeToString(buffer)), nil }