digitaltunnel/secure-code

Cryptographically secure random code generator with a fluent API for Laravel.

Maintainers

Package info

github.com/digital-tunnel/secure-code

pkg:composer/digitaltunnel/secure-code

Statistics

Installs: 12

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.1.0 2026-04-04 20:56 UTC

This package is auto-updated.

Last update: 2026-04-04 20:57:14 UTC


README

Secure Code

Latest Version PHP Version License

Secure Code

Cryptographically secure random code generator with a fluent API for Laravel.

Generate PINs, voucher codes, serial keys, invite tokens, verification codes, sequential document IDs, and more -- all powered by PHP's random_int() CSPRNG under the hood.

Table of Contents

Requirements

Dependency Version
PHP ^8.2
Laravel 11.x, 12.x, 13.x
ext-bcmath *

Installation

composer require digitaltunnel/secure-code

The package auto-discovers its service provider and facade. No manual registration needed.

Publish Configuration (optional)

php artisan vendor:publish --tag=secure-code-config

This publishes config/secure-code.php where you can set application-wide defaults.

Publish Migrations (required for Sequential IDs)

php artisan vendor:publish --tag=secure-code-migrations
php artisan migrate

This creates the secure_code_sequences table used by the sequential document ID generator. Only needed if you use the sequence() feature.

Quick Start

use DigitalTunnel\SecureCode\SecureCode;
use DigitalTunnel\SecureCode\Enums\Charset;

// Generate with defaults (8-char alphanumeric)
$code = SecureCode::generate(); // "a9Kf3mX2"

// 6-digit numeric PIN
$pin = SecureCode::length(6)->charset(Charset::Numeric)->generate(); // "847293"

// Use a preset
$voucher = SecureCode::preset('voucher')->generate(); // "A8K3-M7F2-B9X1-P4J6"

// Pattern-based
$code = SecureCode::pattern('AAA-999-AAA')->generate(); // "KFM-847-XBP"

// Sequential document ID (requires migration)
$id = SecureCode::sequence('invoice')
    ->prefix('INV-')
    ->format('{prefix}{sequence}-{Y}{m}{d}')
    ->padSequence(5)
    ->resetEvery('yearly')
    ->next(); // "INV-00001-20260404"

Configuration

After publishing, edit config/secure-code.php:

return [
    'length' => 8,
    'charset' => 'Alphanumeric',
    'exclude_similar' => false,
    'max_attempts' => 1000,

    'vault' => [
        'ttl' => 300,           // seconds
        'max_attempts' => 5,
        'cache_prefix' => 'secure_code_vault:',
    ],

    'hashid' => [
        'salt' => '',
        'min_length' => 6,
    ],

    'sequences' => [
        'connection' => null,       // null = default DB connection
        'table' => 'secure_code_sequences',
        'pad' => 5,                 // zero-pad width for sequence numbers
        'format' => '{prefix}{sequence}{suffix}',
        'reset' => 'never',         // 'never', 'daily', 'monthly', 'yearly'
        'start_at' => 1,
    ],
];

All options can be overridden per-call via the fluent API.

Usage

Basic Generation

use DigitalTunnel\SecureCode\SecureCode;

$code = SecureCode::generate();
// "a9Kf3mX2"  (8-char alphanumeric by default)

Code Length

SecureCode::length(4)->generate();   // "aK3m"
SecureCode::length(32)->generate();  // "a9Kf3mX2bP7nR4jL8wQ5vT6yU1cE0dG"

Character Sets

The Charset enum provides 13 predefined character pools:

use DigitalTunnel\SecureCode\Enums\Charset;

SecureCode::charset(Charset::Numeric)->generate();            // "84729361"
SecureCode::charset(Charset::Alpha)->generate();              // "aKfmXbPn"
SecureCode::charset(Charset::AlphaUpper)->generate();         // "AKFMXBPN"
SecureCode::charset(Charset::AlphaLower)->generate();         // "akfmxbpn"
SecureCode::charset(Charset::Alphanumeric)->generate();       // "a9Kf3mX2"
SecureCode::charset(Charset::AlphanumericUpper)->generate();  // "A9KF3MX2"
SecureCode::charset(Charset::AlphanumericLower)->generate();  // "a9kf3mx2"
SecureCode::charset(Charset::Hex)->generate();                // "3a7f1b9e"
SecureCode::charset(Charset::HexUpper)->generate();           // "3A7F1B9E"
SecureCode::charset(Charset::Binary)->generate();             // "10110010"
SecureCode::charset(Charset::Base32)->generate();             // "JBSWY3DP"
SecureCode::charset(Charset::Base58)->generate();             // "4K7nR2jL"
SecureCode::charset(Charset::Base64Safe)->generate();         // "aK3m-X_2b"

Available Charsets:

Charset Characters Pool Size
Numeric 0-9 10
Alpha A-Z a-z 52
AlphaUpper A-Z 26
AlphaLower a-z 26
Alphanumeric 0-9 A-Z a-z 62
AlphanumericUpper 0-9 A-Z 36
AlphanumericLower 0-9 a-z 36
Hex 0-9 a-f 16
HexUpper 0-9 A-F 16
Binary 0 1 2
Base32 A-Z 2-7 32
Base58 1-9 A-H J-N P-Z a-k m-z 58
Base64Safe A-Z a-z 0-9 - _ 64

Custom Character Pool

SecureCode::pool('ABCDEF123456')->length(8)->generate();
// "B3A6D1F4"

Prefix & Suffix

SecureCode::length(8)->prefix('INV-')->generate();
// "INV-a9Kf3mX2"

SecureCode::length(8)->suffix('-2026')->generate();
// "a9Kf3mX2-2026"

SecureCode::length(6)->prefix('ORD-')->suffix('-US')->generate();
// "ORD-847293-US"

Prefix and suffix are not counted toward length.

Separators

SecureCode::length(12)->separator('-', 4)->generate();
// "A8K3-M7F2-B9X1"

SecureCode::length(9)->separator(' ', 3)->generate();
// "847 293 615"

Separators are inserted after generation and don't affect the random character count.

Case Forcing

SecureCode::length(10)->charset(Charset::Alpha)->uppercase()->generate();
// "AKFMXBPNRJ"

SecureCode::length(10)->charset(Charset::Alphanumeric)->lowercase()->generate();
// "a9kf3mx2bp"

Exclude Similar Characters

Remove visually ambiguous characters (0, O, 1, I, l) from the pool:

SecureCode::charset(Charset::Alphanumeric)->excludeSimilar()->generate();

Batch Generation

$codes = SecureCode::length(8)->count(10)->generate();
// ['a9Kf3mX2', 'bP7nR4jL', ... ] (array of 10 strings)

When count is 1, a single string is returned (not an array). Codes within a batch are always unique to each other.

Uniqueness Checking

Using a Closure:

$codes = SecureCode::length(10)
    ->count(50)
    ->unique(fn (string $code) => ! DB::table('vouchers')->where('code', $code)->exists())
    ->generate();

Using the UniquenessChecker Interface:

use DigitalTunnel\SecureCode\Contracts\UniquenessChecker;

class VoucherUniquenessChecker implements UniquenessChecker
{
    public function isUnique(string $code): bool
    {
        return ! Voucher::where('code', $code)->exists();
    }
}

$codes = SecureCode::length(10)
    ->count(100)
    ->unique(new VoucherUniquenessChecker())
    ->generate();

Database Uniqueness

Built-in shorthand for database uniqueness -- no manual closure needed:

$code = SecureCode::length(10)
    ->uniqueInTable('vouchers', 'code')
    ->generate();

// With a specific database connection
$code = SecureCode::length(10)
    ->uniqueInTable('vouchers', 'code', 'mysql')
    ->generate();

// Batch of unique codes
$codes = SecureCode::length(10)
    ->count(500)
    ->uniqueInTable('promo_codes', 'code')
    ->generate();

Max Attempts

SecureCode::length(4)
    ->charset(Charset::Numeric)
    ->unique(fn ($code) => ! in_array($code, $existing))
    ->maxAttempts(5000)
    ->generate();

Default: 1000 (configurable in config/secure-code.php).

Presets

Preconfigured templates for common use cases:

use DigitalTunnel\SecureCode\Enums\Preset;

SecureCode::preset('pin')->generate();      // "847293"       (6 numeric digits)
SecureCode::preset('otp')->generate();      // "529184"       (6 numeric digits)
SecureCode::preset('voucher')->generate();  // "A8K3-M7F2-B9X1-P4J6" (16 upper, no similar, dashed)
SecureCode::preset('serial')->generate();   // "3A7F-1B9E-4C2D-8F5A-6E0B" (20 hex, dashed)
SecureCode::preset('api-key')->generate();  // "sk_aK3mX2bP7n..." (40 base64-safe, sk_ prefix)
SecureCode::preset('token')->generate();    // 64 alphanumeric chars
SecureCode::preset('invite')->generate();   // 12 base58 chars

You can also use the enum directly and override options:

SecureCode::preset(Preset::Pin)->length(8)->generate();  // "84729361" (8-digit PIN)

Available Presets:

Preset Length Charset Extras
pin 6 Numeric --
otp 6 Numeric --
voucher 16 AlphanumericUpper excludeSimilar, separator - every 4
serial 20 HexUpper separator - every 4
api-key 40 Base64Safe prefix sk_
token 64 Alphanumeric --
invite 12 Base58 --

Pattern-Based Generation

Define the exact shape of your code using placeholder characters:

Placeholder Produces
A Uppercase letter (A-Z)
a Lowercase letter (a-z)
9 Digit (0-9)
X Hex character (0-9, A-F)
* Any alphanumeric
Anything else Kept as literal
SecureCode::pattern('AAA-999-AAA')->generate();   // "KFM-847-XBP"
SecureCode::pattern('99-AAAA-99')->generate();    // "84-KFMX-72"
SecureCode::pattern('INV-999-AA')->generate();    // "INV-847-KF"
SecureCode::pattern('XXXX-XXXX')->generate();     // "3A7F-1B9E"
SecureCode::pattern('***-***')->generate();        // "a9K-f3m"

// Batch from pattern
$codes = SecureCode::pattern('AAA-999')->count(100)->generate();

Checksum (Luhn & Mod-97)

Append self-validating check digits to generated codes:

Luhn (for numeric codes):

use DigitalTunnel\SecureCode\Support\Checksum;

$code = SecureCode::length(7)
    ->charset(Charset::Numeric)
    ->withChecksum('luhn')
    ->generate();
// "84729355" (7 digits + 1 Luhn check digit = 8 chars)

// Verify
SecureCode::verifyChecksum($code, 'luhn'); // true
Checksum::verifyLuhn($code);               // true

Mod-97 (for alphanumeric codes):

$code = SecureCode::length(6)
    ->charset(Charset::AlphanumericUpper)
    ->withChecksum('mod97')
    ->generate();
// "A8K3MF42" (6 chars + 2 check digits = 8 chars)

SecureCode::verifyChecksum($code, 'mod97'); // true

Use directly:

Checksum::appendLuhn('123456');    // "1234566"
Checksum::verifyLuhn('1234566');   // true
Checksum::appendMod97('ABC123');   // "ABC12374"
Checksum::verifyMod97('ABC12374'); // true

Code Masking

Hide parts of a code for secure display in UIs and logs:

SecureCode::mask('ABCD-EFGH-IJKL');
// "****-****-IJKL"  (last 4 visible, preserves dashes)

SecureCode::mask('ABCD-EFGH-IJKL', visibleEnd: 4, preserve: '-');
// "****-****-IJKL"

SecureCode::mask('ABCDEFGH', visibleStart: 3);
// "ABC*****"

SecureCode::mask('ABCDEFGHIJ', visibleStart: 2, visibleEnd: 2);
// "AB******IJ"

SecureCode::mask('sk_live_abc123def456', character: '#', visibleEnd: 6);
// "##############ef456"

Entropy Calculator

Evaluate the security strength of your code configuration before generating:

$info = SecureCode::length(8)->charset(Charset::Alphanumeric)->entropy();

// [
//     'bits'         => 47.63,
//     'strength'     => 'moderate',
//     'pool_size'    => 62,
//     'length'       => 8,
//     'combinations' => '218340105584896',
// ]

Strength levels:

Bits Strength
< 28 very weak
28-47 weak
48-79 moderate
80-127 strong
128+ very strong

Code Vault (TTL & Verification)

Issue short-lived codes with automatic expiry and brute-force protection -- ideal for email verification, 2FA, and OTP flows:

$vault = SecureCode::vault(
    length: 6,             // code length
    charset: Charset::Numeric,
    ttl: 300,              // expires in 5 minutes
    maxAttempts: 5,        // lock after 5 wrong guesses
);

// Issue a code
$code = $vault->issue('user@example.com');
// "847293" -- stored in cache, expires automatically

// Verify (returns true and auto-revokes on success)
$vault->verify('user@example.com', '847293'); // true
$vault->verify('user@example.com', '847293'); // false (already used)

// Check if a code is pending
$vault->pending('user@example.com'); // bool

// Remaining attempts before lockout
$vault->remainingAttempts('user@example.com'); // int

// Manually revoke
$vault->revoke('user@example.com');

The vault uses timing-safe comparison (hash_equals) and automatically deletes the code after max failed attempts to prevent brute-force attacks.

HashId Encoding

Encode integer IDs into short, obfuscated strings (reversible):

$hashid = SecureCode::hashid(salt: 'my-secret-salt', minLength: 8);

$encoded = $hashid->encode(12345);  // "X8kN3mBp"
$decoded = $hashid->decode($encoded); // 12345

// Different salts produce different encodings
SecureCode::hashid(salt: 'salt-a')->encode(42); // "aBcDeFgH"
SecureCode::hashid(salt: 'salt-b')->encode(42); // "xYzWvUtS"

Export (JSON, CSV, Text)

Generate codes and export them directly:

// JSON
$json = SecureCode::length(8)->count(100)->toJson();
// '["A8K3M7F2","B9X1P4J6",...]'

$json = SecureCode::length(8)->count(100)->toJson(pretty: true);
// Pretty-printed JSON

// CSV
$csv = SecureCode::preset('voucher')->count(50)->toCsv();
// "code\nA8K3-M7F2-B9X1-P4J6\n..."

$csv = SecureCode::length(8)->count(50)->toCsv('voucher_code');
// Custom header

// Plain text (one per line)
$text = SecureCode::length(8)->count(50)->toText();
// "A8K3M7F2\nB9X1P4J6\n..."

Events

Opt-in event dispatching for audit logging:

use DigitalTunnel\SecureCode\Events\CodeGenerated;
use DigitalTunnel\SecureCode\Events\CodeBatchGenerated;

// Enable events
$code = SecureCode::length(8)->withEvents()->generate();
// Dispatches CodeGenerated with $event->code

$codes = SecureCode::length(8)->count(10)->withEvents()->generate();
// Dispatches CodeBatchGenerated with $event->codes and $event->count

Listen for events in your EventServiceProvider or with closures:

Event::listen(CodeGenerated::class, function (CodeGenerated $event) {
    Log::info('Code generated', ['code' => $event->code]);
});

Event::listen(CodeBatchGenerated::class, function (CodeBatchGenerated $event) {
    Log::info('Batch generated', ['count' => $event->count]);
});

Events are not dispatched by default -- you must call withEvents() to opt in.

Validation Rule

Validate incoming codes against format, length, charset, pattern, or checksum:

use DigitalTunnel\SecureCode\Rules\SecureCodeFormat;
use DigitalTunnel\SecureCode\Enums\Charset;

// Validate length
$request->validate([
    'code' => ['required', new SecureCodeFormat(length: 6)],
]);

// Validate charset
$request->validate([
    'code' => ['required', new SecureCodeFormat(charset: Charset::Numeric)],
]);

// Validate length + charset
$request->validate([
    'code' => ['required', new SecureCodeFormat(length: 8, charset: Charset::AlphanumericUpper)],
]);

// Validate against a pattern
$request->validate([
    'code' => ['required', new SecureCodeFormat(pattern: 'AAA-999-AAA')],
]);

// Validate with Luhn checksum
$request->validate([
    'code' => ['required', new SecureCodeFormat(verifyChecksum: true, checksumType: 'luhn')],
]);

// Validate with mod-97 checksum
$request->validate([
    'code' => ['required', new SecureCodeFormat(verifyChecksum: true, checksumType: 'mod97')],
]);

Sequential Document IDs

Generate gap-free, duplicate-free sequential document numbers (invoices, orders, receipts, etc.) backed by database-level locking. Safe for heavy concurrent transactions.

Requires migration: Run php artisan vendor:publish --tag=secure-code-migrations && php artisan migrate first.

Basic usage:

$id = SecureCode::sequence('invoice')
    ->prefix('INV-')
    ->padSequence(5)
    ->next();
// "INV-00001"
// Next call: "INV-00002", "INV-00003", ...

Full format with date tokens:

$id = SecureCode::sequence('invoice')
    ->prefix('INV-')
    ->suffix('-EG')
    ->format('{prefix}{sequence}{separator}{Y}{m}{d}{suffix}')
    ->padSequence(5)
    ->resetEvery('yearly')
    ->next();
// "INV-00001-20260404-EG"

Batch allocation (atomic, contiguous):

$ids = SecureCode::sequence('order')
    ->prefix('ORD-')
    ->format('{prefix}{sequence}{separator}{Y}{m}{d}')
    ->padSequence(5)
    ->next(3);
// ["ORD-00001-20260404", "ORD-00002-20260404", "ORD-00003-20260404"]

Inspect and preview:

$builder = SecureCode::sequence('invoice')->prefix('INV-')->padSequence(5);

// Current value (last allocated), null if never used
$builder->current(); // null

$builder->next();    // "INV-00001"

// Current value after allocation
$builder->current(); // 1

// Preview next without allocating
$builder->preview(); // "INV-00002"

Period-based reset:

// Resets to 1 every year
SecureCode::sequence('invoice')->resetEvery('yearly')->next();

// Resets to 1 every month
SecureCode::sequence('receipt')->resetEvery('monthly')->next();

// Resets to 1 every day
SecureCode::sequence('ticket')->resetEvery('daily')->next();

// Never resets (default)
SecureCode::sequence('order')->resetEvery('never')->next();

Custom start value and DB connection:

$id = SecureCode::sequence('invoice')
    ->startAt(1000)           // first ever allocation starts at 1000
    ->connection('mysql')     // use a specific DB connection
    ->next();
// "01000"

Format tokens:

Token Example Description
{prefix} INV- Configured prefix
{suffix} -EG Configured suffix
{sequence} 00001 Zero-padded sequence number
{separator} - Configured separator
{Y} 2026 4-digit year
{y} 26 2-digit year
{m} 04 2-digit month
{d} 04 2-digit day
{timestamp} 1775433600 Unix timestamp

How it works under the hood:

  • Each call to next() runs an autonomous database transaction with SELECT ... FOR UPDATE
  • This acquires an exclusive row lock, preventing concurrent access
  • The sequence number is committed immediately, independent of any outer transaction
  • No gaps: numbers are contiguous (1, 2, 3, ...)
  • No duplicates: row-level locking serializes access
  • Deadlock-free: each operation locks exactly one row
  • Different sequence keys ('invoice' vs 'order') are fully independent

Tip: For strict isolation from your application's transactions, configure a dedicated database connection in config/secure-code.php under sequences.connection.

Artisan Command

Generate codes from the command line:

# Default (8-char alphanumeric)
php artisan secure-code:generate

# Custom options
php artisan secure-code:generate --length=16 --charset=Numeric

# Batch
php artisan secure-code:generate --count=50

# With preset
php artisan secure-code:generate --preset=voucher

# With pattern
php artisan secure-code:generate --pattern="AAA-999-AAA"

# Formatted output
php artisan secure-code:generate --count=10 --json
php artisan secure-code:generate --count=10 --csv

# All options
php artisan secure-code:generate \
    --length=12 \
    --charset=AlphanumericUpper \
    --prefix=INV- \
    --suffix=-2026 \
    --separator=- \
    --every=4 \
    --upper \
    --exclude-similar \
    --checksum \
    --count=20

Blade Directive

Quick inline generation in Blade templates:

<p>Your code: @securecode</p>

Facade

The package registers a facade automatically:

use DigitalTunnel\SecureCode\Facades\SecureCode;

SecureCode::length(8)->generate();

Combining Options

All methods are chainable and can be freely combined:

$vouchers = SecureCode::length(16)
    ->charset(Charset::AlphanumericUpper)
    ->excludeSimilar()
    ->separator('-', 4)
    ->prefix('GIFT-')
    ->suffix('-2026')
    ->withChecksum('mod97')
    ->withEvents()
    ->count(500)
    ->uniqueInTable('vouchers', 'code')
    ->maxAttempts(2000)
    ->generate();

API Reference

SecureCode (Static Entry Point)

Method Returns Description
generate() string|array Generate with default config
length(int) CodeBuilder Set code length
charset(Charset) CodeBuilder Set character set
pool(string) CodeBuilder Set custom character pool
prefix(string) CodeBuilder Set prefix
suffix(string) CodeBuilder Set suffix
separator(string, int) CodeBuilder Set separator and interval
uppercase() CodeBuilder Force uppercase
lowercase() CodeBuilder Force lowercase
excludeSimilar(bool) CodeBuilder Exclude ambiguous characters
count(int) CodeBuilder Set batch size
unique(Closure|UniquenessChecker) CodeBuilder Set uniqueness checker
uniqueInTable(string, string, ?string) CodeBuilder Database uniqueness
maxAttempts(int) CodeBuilder Set max retry attempts
preset(string|Preset) CodeBuilder Apply a preset
pattern(string) CodeBuilder Set generation pattern
withChecksum(string) CodeBuilder Append checksum digit
withEvents(bool) CodeBuilder Enable event dispatching
mask(string, ...) string Mask a code for display
verifyChecksum(string, string) bool Verify a checksum
vault(int, Charset, int, int) CodeVault Create a code vault
hashid(string, int) HashId Create a HashId encoder
sequence(string) SequenceBuilder Create a sequential ID builder

CodeBuilder (Fluent Builder)

Immutable -- every method returns a new instance:

$template = SecureCode::length(12)->charset(Charset::AlphanumericUpper)->separator('-', 4);

$code1 = $template->generate();                    // uses template
$code2 = $template->prefix('VIP-')->generate();    // extends template safely

Additional methods on CodeBuilder:

Method Returns Description
toJson(bool $pretty) string Generate + export as JSON
toCsv(string $header) string Generate + export as CSV
toText() string Generate + export as text
entropy() array Calculate entropy info

Support Classes

Class Description
Checksum::appendLuhn(string) Append Luhn check digit
Checksum::verifyLuhn(string) Verify Luhn checksum
Checksum::appendMod97(string) Append mod-97 check digits
Checksum::verifyMod97(string) Verify mod-97 checksum
Mask::apply(string, ...) Mask a code string
Entropy::calculate(int, int) Calculate entropy bits
Entropy::strength(float) Get strength label
Export::toJson(array, bool) Export as JSON
Export::toCsv(array, string) Export as CSV
Export::toText(array) Export as plain text
PatternGenerator::generate(string) Generate from pattern
PatternGenerator::toRegex(string) Convert pattern to regex
HashId::encode(int) Encode integer
HashId::decode(string) Decode to integer

SequenceBuilder (Fluent Builder)

Immutable -- every method returns a new instance:

Method Returns Description
prefix(string) SequenceBuilder Set ID prefix
suffix(string) SequenceBuilder Set ID suffix
separator(string) SequenceBuilder Set separator character
format(string) SequenceBuilder Set format template with tokens
padSequence(int) SequenceBuilder Set zero-pad width
resetEvery(string|SequenceResetPeriod) SequenceBuilder Set reset period
startAt(int) SequenceBuilder Set initial sequence value
connection(string) SequenceBuilder Set database connection
date(DateTimeInterface) SequenceBuilder Set date for formatting and period key
next(int $count = 1) string|array Allocate and return next ID(s)
current() ?int Get last allocated value
preview() string Preview next ID without allocating

Real-World Examples

Email Verification Flow

$vault = SecureCode::vault(ttl: 600);  // 10 min expiry
$code = $vault->issue($user->email);
Mail::to($user)->send(new VerificationMail($code));

// Later, when user submits the code:
if ($vault->verify($user->email, $request->code)) {
    $user->markEmailAsVerified();
}

Gift Card with Self-Validating Checksum

$card = SecureCode::preset('voucher')
    ->withChecksum('mod97')
    ->uniqueInTable('gift_cards', 'code')
    ->generate();

// When redeeming:
if (! SecureCode::verifyChecksum($request->code, 'mod97')) {
    abort(422, 'Invalid gift card format.');
}

Obfuscated Order URLs

$hashid = SecureCode::hashid(salt: config('app.key'));

// Generate URL
$url = route('orders.show', $hashid->encode($order->id));
// /orders/X8kN3mBp

// Resolve in controller
$orderId = $hashid->decode($request->route('order'));

Batch Promo Codes Export

$csv = SecureCode::preset('voucher')
    ->count(10000)
    ->uniqueInTable('promo_codes', 'code')
    ->toCsv('promo_code');

Storage::put('exports/promo-codes.csv', $csv);

Security Audit with Entropy Check

$info = SecureCode::length(16)->charset(Charset::Base58)->entropy();

if ($info['bits'] < 80) {
    throw new \RuntimeException('Insufficient entropy for production tokens.');
}

Two-Factor Authentication

$vault = SecureCode::vault(length: 6, ttl: 120, maxAttempts: 3);
$code = $vault->issue($user->id);

// Display masked after generation
SecureCode::mask($code, visibleStart: 1, visibleEnd: 1);
// "8****3"

Invoice Numbers (Sequential)

// Sequential, gap-free invoice numbers that reset yearly
$invoiceId = SecureCode::sequence('invoice')
    ->prefix('INV-')
    ->suffix('-EG')
    ->format('{prefix}{sequence}{separator}{Y}{m}{d}{suffix}')
    ->padSequence(6)
    ->resetEvery('yearly')
    ->next();
// "INV-000001-20260404-EG"
// Next: "INV-000002-20260404-EG"

// Bulk-generate for a batch of orders
$ids = SecureCode::sequence('order')
    ->prefix('ORD-')
    ->format('{prefix}{Y}{m}{separator}{sequence}')
    ->padSequence(5)
    ->resetEvery('monthly')
    ->next(10);
// ["ORD-202604-00001", "ORD-202604-00002", ..., "ORD-202604-00010"]

Architecture

src/
├── SecureCode.php              # Static entry point
├── CodeBuilder.php             # Immutable fluent builder
├── CodeGenerator.php           # CSPRNG engine (random_int)
├── CodeVault.php               # TTL-based issue/verify/revoke
├── Contracts/
│   └── UniquenessChecker.php   # Interface for uniqueness logic
├── Console/
│   └── GenerateCommand.php     # Artisan command
├── Enums/
│   ├── Charset.php             # 13 predefined character pools
│   ├── Preset.php              # 7 preconfigured presets
│   └── SequenceResetPeriod.php # Reset period enum
├── Events/
│   ├── CodeGenerated.php       # Single code event
│   └── CodeBatchGenerated.php  # Batch event
├── Facades/
│   └── SecureCode.php          # Laravel Facade
├── Providers/
│   └── SecureCodeServiceProvider.php
├── Rules/
│   └── SecureCodeFormat.php    # Validation rule
├── Sequence/
│   ├── SequenceBuilder.php     # Fluent builder for sequential IDs
│   ├── SequenceFormatter.php   # Token-based string formatting
│   └── SequenceGenerator.php   # Atomic DB allocation engine
└── Support/
    ├── Checksum.php            # Luhn & Mod-97
    ├── Entropy.php             # Entropy calculator
    ├── Export.php              # JSON, CSV, Text export
    ├── HashId.php              # Integer encoding/decoding
    ├── Mask.php                # Code masking
    └── PatternGenerator.php    # Pattern-based generation

Security: All randomness is produced by random_int(), which draws from the OS CSPRNG. The Code Vault uses hash_equals for timing-safe comparison.

Immutability: CodeBuilder clones itself on every fluent call. Safe to store and reuse as templates.

Testing

The package ships with 171 Pest tests covering every feature:

./vendor/bin/pest

Security

If you discover a security vulnerability, please send an email to hey@digitaltunnel.net instead of opening a public issue.

See SECURITY.md for full details on our security policy, supported versions, and best practices.

License

The MIT License (MIT). See LICENSE for details.