diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml new file mode 100644 index 0000000..a96e36b --- /dev/null +++ b/.github/workflows/codeql-analysis.yaml @@ -0,0 +1,86 @@ +# Copyright (c) 2024 Six After, Inc +# +# This source code is licensed under the Apache 2.0 License found in the +# LICENSE file in the root directory of this source tree. + +# Ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +name: "CodeQL Analysis" + +permissions: + contents: read + +on: + workflow_dispatch: + + push: + branches: + - main + + paths-ignore: + - 'CHANGELOG/**' + - 'CODEOWNERS' + - 'docs/**' + - 'LICENSE' + - '**/*.md' + + schedule: + # * * * * * + # | | | | | + # | | | | day of the week (0–6) (Sunday to Saturday; + # | | | month (1–12) 7 is also Sunday on some systems) + # | | day of the month (1–31) + # | hour (0–23) + # minute (0–59) + - cron: '0 0 * * 3' + +jobs: + analyze: + runs-on: ubuntu-latest + + strategy: + matrix: + include: + - language: go + + permissions: + security-events: write + + steps: + - name: Preamble + run: | + whoami + echo github ref $GITHUB_REF + echo workflow $GITHUB_WORKFLOW + echo home $HOME + echo event name $GITHUB_EVENT_NAME + echo workspace $GITHUB_WORKSPACE + + df -h + + sudo DEBIAN_FRONTEND=noninteractive apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common ca-certificates gnupg-agent curl build-essential make + + # Ref: https://github.com/actions/checkout + - name: Checkout Source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Ref: https://github.com/actions/setup-go + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # Ref: https://github.com/github/codeql-action + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/CHANGELOG/CHANGELOG-1.x.md b/CHANGELOG/CHANGELOG-1.x.md index f91eb00..e10927a 100644 --- a/CHANGELOG/CHANGELOG-1.x.md +++ b/CHANGELOG/CHANGELOG-1.x.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Date format: `YYYY-MM-DD` + --- ## [Unreleased] @@ -16,7 +18,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --- -## [1.1.0] - 2024-OCT-24 +## [1.2.0] - 2024-11-15 + +### Added +### Changed +- **DEBT:** Added missing license header to the CodeQL analysis configuration file. +- **DEBT:** Refactored CHANGELOG date format to `YYYY-MM-DD`. +- **DEBT:** Upgraded to [sixafter/nanoid@v1.17.3](https://github.com/sixafter/nanoid/releases/tag/v1.18.1). + +### Deprecated +### Removed +### Fixed +### Security + +--- +## [1.1.0] - 2024-11-14 + +### Added +### Changed +- **DEBT:** Upgraded to [sixafter/nanoid@v1.17.3](https://github.com/sixafter/nanoid/releases/tag/v1.17.3). + +### Deprecated +### Removed +### Fixed +### Security + +--- +## [1.1.0] - 2024-11-14 ### Added ### Changed @@ -27,7 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --- -## [1.0.1] - 2024-OCT-24 +## [1.0.1] - 2024-11-14 ### Added - **FEATURE:** Added support for Homebrew. @@ -38,7 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --- -## [1.0.0] - 2024-OCT-24 +## [1.0.0] - 2024-11-13 ### Added - **FEATURE:** Initial commit. @@ -48,7 +76,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Security -[Unreleased]: https://github.com/sixafter/nanoid-cli/compare/v1.1.0..HEAD +[Unreleased]: https://github.com/sixafter/nanoid-cli/compare/v1.2.0..HEAD +[1.2.0]: https://github.com/sixafter/nanoid-cli/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/sixafter/nanoid-cli/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/sixafter/nanoid-cli/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/sixafter/nanoid-cli/compare/a6a1eb74b61e518fd0216a17dfe5c9b4c432e6e8...v1.0.0 diff --git a/Makefile b/Makefile index 7cee0eb..ff1902d 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ GO_WORK_FILE := ./go.work export BINARY_NAME=out/nanoid .PHONY: all -all: clean test +all: clean build test .PHONY: build build: ## Build the binary executable. diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go index 58438e0..4eb91e1 100644 --- a/cmd/generate/generate.go +++ b/cmd/generate/generate.go @@ -91,7 +91,7 @@ func runGenerate(cmd *cobra.Command, args []string) error { return fmt.Errorf("error generating Nano ID: %w", err) } - _, err = writer.WriteString(id + "\n") + _, err = writer.WriteString(id.String() + "\n") if err != nil { return fmt.Errorf("error writing Nano ID: %w", err) } diff --git a/go.mod b/go.mod index 02d8814..220123a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23 require ( github.com/blang/semver/v4 v4.0.0 - github.com/sixafter/nanoid v1.17.3 + github.com/sixafter/nanoid v1.18.1 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index f70c608..104eee4 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sixafter/nanoid v1.17.3 h1:0xDxzu01jqeQM2wZP92UIyHia7h+Nn5UbIMrCGlxAoA= -github.com/sixafter/nanoid v1.17.3/go.mod h1:dQjvP7lwL/M43aOsHeN09O5+5/2SKzatkjvJDJBTeVE= +github.com/sixafter/nanoid v1.18.1 h1:qIO2B3JMdbgedLW1+IbTABAr4i7h3HY/YnyuvENQgq0= +github.com/sixafter/nanoid v1.18.1/go.mod h1:dQjvP7lwL/M43aOsHeN09O5+5/2SKzatkjvJDJBTeVE= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/vendor/github.com/sixafter/nanoid/.goreleaser.yaml b/vendor/github.com/sixafter/nanoid/.goreleaser.yaml index 17623f6..fa1b154 100644 --- a/vendor/github.com/sixafter/nanoid/.goreleaser.yaml +++ b/vendor/github.com/sixafter/nanoid/.goreleaser.yaml @@ -84,7 +84,7 @@ sboms: release: name_template: 'v{{ .Version }}' footer: | - **Full Changelog**: [https://github.com/sixafter/nanoid/tree/main/CHANGELOG/CHANGELOG-1.x](https://github.com/sixafter/nanoid/tree/main/CHANGELOG/CHANGELOG-1.x.md) + **Full Changelog**: [CHANGELOG](https://github.com/sixafter/nanoid/tree/main/CHANGELOG/CHANGELOG-1.x.md) ## Where to go next? diff --git a/vendor/github.com/sixafter/nanoid/Makefile b/vendor/github.com/sixafter/nanoid/Makefile index 9a3900f..9bc1efa 100644 --- a/vendor/github.com/sixafter/nanoid/Makefile +++ b/vendor/github.com/sixafter/nanoid/Makefile @@ -36,7 +36,7 @@ bench: ## Execute benchmark tests .PHONY: clean clean: ## Remove previous build - $(GO_CLEAN) + $(GO_CLEAN) ./... .PHONY: cover cover: ## Generate global code coverage report diff --git a/vendor/github.com/sixafter/nanoid/nanoid.go b/vendor/github.com/sixafter/nanoid/nanoid.go index 199bb63..5200d28 100644 --- a/vendor/github.com/sixafter/nanoid/nanoid.go +++ b/vendor/github.com/sixafter/nanoid/nanoid.go @@ -20,8 +20,82 @@ import ( "github.com/sixafter/nanoid/x/crypto/prng" ) -// DefaultGenerator is a global, shared instance of a Nano ID generator. It is safe for concurrent use. -var DefaultGenerator Generator +var ( + // DefaultGenerator is a global, shared instance of a Nano ID generator. It is safe for concurrent use. + DefaultGenerator Generator + + // DefaultRandReader is the default random number generator used for generating IDs. + DefaultRandReader = prng.Reader + + // ErrDuplicateCharacters is returned when the provided alphabet contains duplicate characters. + ErrDuplicateCharacters = errors.New("duplicate characters in alphabet") + + // ErrExceededMaxAttempts is returned when the maximum number of attempts to perform + // an operation, such as generating a unique ID, has been exceeded. + ErrExceededMaxAttempts = errors.New("exceeded maximum attempts") + + // ErrInvalidLength is returned when a specified length value for an operation is invalid. + ErrInvalidLength = errors.New("invalid length") + + // ErrInvalidAlphabet is returned when the provided alphabet for generating IDs is invalid. + ErrInvalidAlphabet = errors.New("invalid alphabet") + + // ErrNonUTF8Alphabet is returned when the provided alphabet contains non-UTF-8 characters. + ErrNonUTF8Alphabet = errors.New("alphabet contains invalid UTF-8 characters") + + // ErrAlphabetTooShort is returned when the provided alphabet has fewer than 2 characters. + ErrAlphabetTooShort = errors.New("alphabet length is less than 2") + + // ErrAlphabetTooLong is returned when the provided alphabet exceeds 256 characters. + ErrAlphabetTooLong = errors.New("alphabet length exceeds 256") + + // ErrNilRandReader is returned when the random number generator (rand.Reader) is nil, + // preventing the generation of random values. + ErrNilRandReader = errors.New("nil random reader") +) + +const ( + // DefaultAlphabet defines the standard set of characters used for Nano ID generation. + // It includes uppercase and lowercase English letters, digits, and the characters + // '_' and '-'. This selection aligns with the Nano ID specification, ensuring + // a URL-friendly and easily readable identifier. + // + // Example: "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + DefaultAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + // DefaultLength specifies the default number of characters in a generated Nano ID. + // A length of 21 characters provides a high level of uniqueness while maintaining + // brevity, making it suitable for most applications requiring unique identifiers. + DefaultLength = 21 + + // maxAttemptsMultiplier determines the maximum number of attempts the generator + // will make to produce a valid Nano ID before failing. It is calculated as a + // multiplier based on the desired ID length to balance between performance + // and the probability of successful ID generation, especially when using + // non-power-of-two alphabets. + maxAttemptsMultiplier = 10 + + // MinAlphabetLength sets the minimum permissible number of unique characters + // in the alphabet used for Nano ID generation. An alphabet with fewer than + // 2 characters would not provide sufficient variability for generating unique IDs, + // making this a lower bound to ensure meaningful ID generation. + // + // Example: An alphabet like "AB" is acceptable, but "A" is not. + MinAlphabetLength = 2 + + // MaxAlphabetLength defines the maximum allowable number of unique characters + // in the alphabet for Nano ID generation. This upper limit ensures that the + // generator operates within reasonable memory and performance constraints, + // preventing excessively large alphabets that could degrade performance or + // complicate index calculations. + MaxAlphabetLength = 256 +) + +// ID represents a Nano ID as a string. +type ID string + +// EmptyID represents an empty Nano ID. +var EmptyID = ID("") func init() { var err error @@ -35,7 +109,7 @@ func init() { // It is used with the Function Options pattern. type ConfigOptions struct { // RandReader is the source of randomness used for generating IDs. - // By default, it uses crypto/rand.Reader, which provides cryptographically secure random bytes. + // By default, it uses x/crypto/prng/Reader, which provides cryptographically secure random bytes. RandReader io.Reader // Alphabet is the set of characters used to generate the Nano ID. @@ -166,7 +240,7 @@ type Generator interface { // // handle error // } // fmt.Println("Generated ID:", id) - New(length int) (string, error) + New(length int) (ID, error) // Read fills the provided byte slice 'p' with random data, reading up to len(p) bytes. // Returns the number of bytes read and any error encountered during the read operation. @@ -221,7 +295,7 @@ type generator struct { // // handle error // } // fmt.Println("Generated ID:", id) -func New() (string, error) { +func New() (ID, error) { return NewWithLength(DefaultLength) } @@ -238,7 +312,7 @@ func New() (string, error) { // // handle error // } // fmt.Println("Generated ID:", id) -func NewWithLength(length int) (string, error) { +func NewWithLength(length int) (ID, error) { return DefaultGenerator.New(length) } @@ -251,7 +325,7 @@ func NewWithLength(length int) (string, error) { // // id := nanoid.Must() // fmt.Println("Generated ID:", id) -func Must() string { +func Must() ID { return MustWithLength(DefaultLength) } @@ -268,7 +342,7 @@ func Must() string { // // id := nanoid.MustWithLength(30) // fmt.Println("Generated ID:", id) -func MustWithLength(length int) string { +func MustWithLength(length int) ID { id, err := NewWithLength(length) if err != nil { panic(err) @@ -312,54 +386,6 @@ func Read(p []byte) (n int, err error) { return DefaultGenerator.Read(p) } -var ( - ErrDuplicateCharacters = errors.New("duplicate characters in alphabet") - ErrExceededMaxAttempts = errors.New("exceeded maximum attempts") - ErrInvalidLength = errors.New("invalid length") - ErrInvalidAlphabet = errors.New("invalid alphabet") - ErrNonUTF8Alphabet = errors.New("alphabet contains invalid UTF-8 characters") - ErrAlphabetTooShort = errors.New("alphabet length is less than 2") - ErrAlphabetTooLong = errors.New("alphabet length exceeds 256") - ErrNilRandReader = errors.New("nil random reader") -) - -const ( - // DefaultAlphabet defines the standard set of characters used for Nano ID generation. - // It includes uppercase and lowercase English letters, digits, and the characters - // '_' and '-'. This selection aligns with the Nano ID specification, ensuring - // a URL-friendly and easily readable identifier. - // - // Example: "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - DefaultAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - - // DefaultLength specifies the default number of characters in a generated Nano ID. - // A length of 21 characters provides a high level of uniqueness while maintaining - // brevity, making it suitable for most applications requiring unique identifiers. - DefaultLength = 21 - - // maxAttemptsMultiplier determines the maximum number of attempts the generator - // will make to produce a valid Nano ID before failing. It is calculated as a - // multiplier based on the desired ID length to balance between performance - // and the probability of successful ID generation, especially when using - // non-power-of-two alphabets. - maxAttemptsMultiplier = 10 - - // MinAlphabetLength sets the minimum permissible number of unique characters - // in the alphabet used for Nano ID generation. An alphabet with fewer than - // 2 characters would not provide sufficient variability for generating unique IDs, - // making this a lower bound to ensure meaningful ID generation. - // - // Example: An alphabet like "AB" is acceptable, but "A" is not. - MinAlphabetLength = 2 - - // MaxAlphabetLength defines the maximum allowable number of unique characters - // in the alphabet for Nano ID generation. This upper limit ensures that the - // generator operates within reasonable memory and performance constraints, - // preventing excessively large alphabets that could degrade performance or - // complicate index calculations. - MaxAlphabetLength = 256 -) - // Option defines a function type for configuring the Generator. // It allows for flexible and extensible configuration by applying // various settings to the ConfigOptions during Generator initialization. @@ -455,7 +481,7 @@ func NewGenerator(options ...Option) (Generator, error) { // and the default length hint for ID generation. configOpts := &ConfigOptions{ Alphabet: DefaultAlphabet, - RandReader: prng.Reader, + RandReader: DefaultRandReader, LengthHint: DefaultLength, } @@ -698,9 +724,9 @@ func (g *generator) processRandomBytes(randomBytes []byte, i int) uint { // // handle error // } // fmt.Println("Generated ID:", id) -func (g *generator) New(length int) (string, error) { +func (g *generator) New(length int) (ID, error) { if length <= 0 { - return "", ErrInvalidLength + return EmptyID, ErrInvalidLength } if g.config.isASCII { @@ -710,7 +736,7 @@ func (g *generator) New(length int) (string, error) { } // newASCII generates a new Nano ID using the ASCII alphabet. -func (g *generator) newASCII(length int) (string, error) { +func (g *generator) newASCII(length int) (ID, error) { randomBytesPtr := g.entropyPool.Get().(*[]byte) randomBytes := *randomBytesPtr bufferLen := len(randomBytes) @@ -738,7 +764,7 @@ func (g *generator) newASCII(length int) (string, error) { // Fill the random bytes buffer if _, err := g.config.randReader.Read(randomBytes[:neededBytes]); err != nil { - return "", err + return EmptyID, err } // Process each segment of random bytes @@ -755,14 +781,14 @@ func (g *generator) newASCII(length int) (string, error) { // Check for max attempts if cursor < length { - return "", ErrExceededMaxAttempts + return EmptyID, ErrExceededMaxAttempts } - return sb.String(), nil + return ID(sb.String()), nil } // newUnicode generates a new Nano ID using the Unicode alphabet. -func (g *generator) newUnicode(length int) (string, error) { +func (g *generator) newUnicode(length int) (ID, error) { // Retrieve random bytes from the pool randomBytesPtr := g.entropyPool.Get().(*[]byte) randomBytes := *randomBytesPtr @@ -792,7 +818,7 @@ func (g *generator) newUnicode(length int) (string, error) { // Fill the random bytes buffer if _, err := g.config.randReader.Read(randomBytes[:neededBytes]); err != nil { - return "", err + return EmptyID, err } // Process each segment of random bytes @@ -809,10 +835,10 @@ func (g *generator) newUnicode(length int) (string, error) { // Check for max attempts if cursor < length { - return "", ErrExceededMaxAttempts + return EmptyID, ErrExceededMaxAttempts } - return sb.String(), nil + return ID(sb.String()), nil } // Reader is the interface that wraps the basic Read method. @@ -861,6 +887,128 @@ func (g *generator) Read(p []byte) (n int, err error) { return length, nil } +// IsEmpty returns true if the ID is an empty ID (EmptyID) +func (id ID) IsEmpty() bool { + return id.Compare(EmptyID) == 0 +} + +// Compare compares two IDs lexicographically and returns an integer. +// The result will be 0 if id==other, -1 if id < other, and +1 if id > other. +// +// Parameters: +// - other ID: The ID to compare against. +// +// Returns: +// - int: An integer indicating the comparison result. +// +// Usage: +// +// id1 := ID("V1StGXR8_Z5jdHi6B-myT") +// id2 := ID("V1StGXR8_Z5jdHi6B-myT") +// result := id1.Compare(id2) +// fmt.Println(result) // Output: 0 +func (id ID) Compare(other ID) int { + return strings.Compare(string(id), string(other)) +} + +// String returns the string representation of the ID. +// It implements the fmt.Stringer interface, allowing the ID to be +// used seamlessly with fmt package functions like fmt.Println and fmt.Printf. +// +// Example: +// +// id := Must() +// fmt.Println(id) // Output: V1StGXR8_Z5jdHi6B-myT +func (id ID) String() string { + return string(id) +} + +// MarshalText converts the ID to a byte slice. +// It implements the encoding.TextMarshaler interface, enabling the ID +// to be marshaled into text-based formats such as XML and YAML. +// +// Returns: +// - A byte slice containing the ID. +// - An error if the marshaling fails. +// +// Example: +// +// id := Must() +// text, err := id.MarshalText() +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(string(text)) // Output: V1StGXR8_Z5jdHi6B-myT +func (id ID) MarshalText() ([]byte, error) { + return []byte(id), nil +} + +// UnmarshalText parses a byte slice and assigns the result to the ID. +// It implements the encoding.TextUnmarshaler interface, allowing the ID +// to be unmarshaled from text-based formats. +// +// Parameters: +// - text: A byte slice containing the ID data. +// +// Returns: +// - An error if the unmarshaling fails. +// +// Example: +// +// var id ID +// err := id.UnmarshalText([]byte("new-id")) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(id) // Output: new-id +func (id *ID) UnmarshalText(text []byte) error { + *id = ID(text) + return nil +} + +// MarshalBinary converts the ID to a byte slice. +// It implements the encoding.BinaryMarshaler interface, enabling the ID +// to be marshaled into binary formats for efficient storage or transmission. +// +// Returns: +// - A byte slice containing the ID. +// - An error if the marshaling fails. +// +// Example: +// +// id := Must() +// binaryData, err := id.MarshalBinary() +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(binaryData) // Output: [86 49 83 116 71 88 82 56 95 90 ...] +func (id ID) MarshalBinary() ([]byte, error) { + return []byte(id), nil +} + +// UnmarshalBinary parses a byte slice and assigns the result to the ID. +// It implements the encoding.BinaryUnmarshaler interface, allowing the ID +// to be unmarshaled from binary formats. +// +// Parameters: +// - data: A byte slice containing the binary ID data. +// +// Returns: +// - An error if the unmarshaling fails. +// +// Example: +// +// var id ID +// err := id.UnmarshalBinary([]byte{86, 49, 83, 116, 71, 88, 82, 56, 95, 90}) // "V1StGXR8_Z5jdHi6B-myT" +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(id) // Output: V1StGXR8_Z5jdHi6B-myT +func (id *ID) UnmarshalBinary(data []byte) error { + *id = ID(data) + return nil +} + // Config holds the runtime configuration for the Nano ID generator. // // It is immutable after initialization and provides all the necessary diff --git a/vendor/modules.txt b/vendor/modules.txt index d0b6af4..e2a03b9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -10,7 +10,7 @@ github.com/inconshreveable/mousetrap # github.com/pmezard/go-difflib v1.0.0 ## explicit github.com/pmezard/go-difflib/difflib -# github.com/sixafter/nanoid v1.17.3 +# github.com/sixafter/nanoid v1.18.1 ## explicit; go 1.23 github.com/sixafter/nanoid github.com/sixafter/nanoid/x/crypto/prng