diff --git a/.env.example b/.env.example index 8f84714..fc60f5a 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,11 @@ APP_MAINTENANCE_DRIVER=file PHP_CLI_SERVER_WORKERS=12 -BCRYPT_ROUNDS=12 +HASH_DRIVER=argon2id +HASH_VERIFY=true +ARGON_MEMORY=65536 +ARGON_THREADS=1 +ARGON_TIME=4 LOG_CHANNEL=stack LOG_STACK=single @@ -30,6 +34,9 @@ DB_PASSWORD=password SESSION_DRIVER=database SESSION_LIFETIME=120 SESSION_ENCRYPT=false +SESSION_SECURE_COOKIE=false +SESSION_HTTP_ONLY=true +SESSION_SAME_SITE=lax SESSION_PATH=/ SESSION_DOMAIN=null @@ -69,6 +76,10 @@ AUTH_ENABLE_REMEMBER_ME=true AUTH_ENABLE_EMAIL_VERIFICATION=false AUTH_ENABLE_TWO_FACTOR=false AUTH_REQUIRE_TWO_FACTOR=false +AUTH_TWO_FACTOR_CHALLENGE_TIMEOUT=300 +AUTH_ABSOLUTE_SESSION_LIFETIME=28800 +AUTH_SECURITY_LOG_CHANNEL=stack +# In production, set SESSION_ENCRYPT=true and SESSION_SECURE_COOKIE=true. # Auth Redirects # AUTH_REDIRECT_LOGIN=/dashboard diff --git a/app/Http/Controllers/Auth/CompleteProfileController.php b/app/Http/Controllers/Auth/CompleteProfileController.php index 6ebca24..59f2bc7 100644 --- a/app/Http/Controllers/Auth/CompleteProfileController.php +++ b/app/Http/Controllers/Auth/CompleteProfileController.php @@ -5,6 +5,8 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Http\Requests\Auth\CompleteProfileRequest; use App\Models\User; +use App\Services\Auth\SecurityEventRecorder; +use App\Services\Auth\TwoFactorChallenge; use Illuminate\Auth\Events\Registered; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -33,8 +35,11 @@ class CompleteProfileController extends Controller /** * Handle the complete profile request. */ - public function store(CompleteProfileRequest $request): RedirectResponse - { + public function store( + CompleteProfileRequest $request, + TwoFactorChallenge $twoFactor, + SecurityEventRecorder $securityEvents + ): RedirectResponse { $socialiteUser = session('socialite_user'); $validated = $request->validated(); @@ -58,6 +63,16 @@ class CompleteProfileController extends Controller Auth::login($user, remember: true); $request->session()->regenerate(); + $securityEvents->record('login.social_succeeded', $user, $request); + + if ($twoFactor->enrollmentRequiredFor($user)) { + $request->session()->put( + 'url.intended', + config('auth-ui.redirects.login', '/') + ); + + return redirect()->route('two-factor.setup'); + } return redirect()->intended(config('auth-ui.redirects.login', '/')); } diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index b5146ad..b452fd8 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Http\Requests\Auth\ForgotPasswordRequest; +use App\Services\Auth\SecurityEventRecorder; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Password; use Inertia\Inertia; @@ -26,16 +27,20 @@ class ForgotPasswordController extends Controller /** * Handle an incoming password reset link request. */ - public function store(ForgotPasswordRequest $request): RedirectResponse - { - $status = Password::sendResetLink( + public function store( + ForgotPasswordRequest $request, + SecurityEventRecorder $securityEvents + ): RedirectResponse { + Password::sendResetLink( $request->validated() ); + $securityEvents->record('password_reset.requested', null, $request, [ + 'email_hash' => hash('sha256', strtolower($request->validated('email'))), + ]); - if ($status === Password::RESET_LINK_SENT) { - return back()->with('status', __($status)); - } - - return back()->withErrors(['email' => __($status)]); + return back()->with( + 'status', + 'If an account exists for that email address, a password reset link has been sent.' + ); } } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index c589c87..1a947dd 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -5,13 +5,17 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Http\Requests\Auth\LoginRequest; use App\Models\User; +use App\Services\Auth\SecurityEventRecorder; use App\Services\Auth\TwoFactorChallenge; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Inertia\Inertia; use Inertia\Response; +use RuntimeException; class LoginController extends Controller { @@ -26,8 +30,11 @@ class LoginController extends Controller /** * Handle an incoming authentication request. */ - public function store(LoginRequest $request, TwoFactorChallenge $twoFactor): RedirectResponse - { + public function store( + LoginRequest $request, + TwoFactorChallenge $twoFactor, + SecurityEventRecorder $securityEvents + ): RedirectResponse { $login = $request->validated('login'); $password = $request->validated('password'); @@ -46,7 +53,19 @@ class LoginController extends Controller ? $provider->retrieveByCredentials($credentials) : null; - if (! $user || ! $provider->validateCredentials($user, $credentials)) { + try { + $credentialsAreValid = $user + ? $provider->validateCredentials($user, $credentials) + : Hash::check($password, $this->dummyPasswordHash()); + } catch (RuntimeException) { + $credentialsAreValid = false; + } + + if (! $user || ! $credentialsAreValid) { + $securityEvents->record('login.password_failed', $user, $request, [ + 'login_hash' => hash('sha256', strtolower($login)), + ]); + throw ValidationException::withMessages([ 'login' => __('auth.failed'), ]); @@ -58,11 +77,23 @@ class LoginController extends Controller } if ($twoFactor->requiredFor($user)) { + $securityEvents->record('login.password_succeeded_pending_two_factor', $user, $request); + return $twoFactor->begin($request, $user, $remember); } Auth::login($user, $remember); $request->session()->regenerate(); + $securityEvents->record('login.succeeded', $user, $request); + + if ($twoFactor->enrollmentRequiredFor($user)) { + $request->session()->put( + 'url.intended', + config('auth-ui.redirects.login', '/') + ); + + return redirect()->route('two-factor.setup'); + } return redirect()->intended(config('auth-ui.redirects.login', '/')); } @@ -72,6 +103,10 @@ class LoginController extends Controller */ public function destroy(Request $request): RedirectResponse { + /** @var User|null $user */ + $user = $request->user(); + app(SecurityEventRecorder::class)->record('logout.succeeded', $user, $request); + Auth::logout(); $request->session()->invalidate(); @@ -79,4 +114,11 @@ class LoginController extends Controller return redirect(config('auth-ui.redirects.logout', '/')); } + + private function dummyPasswordHash(): string + { + static $hash; + + return $hash ??= Hash::make(Str::random(64)); + } } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index ea6ed3c..7000f41 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -5,6 +5,8 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Http\Requests\Auth\RegisterRequest; use App\Models\User; +use App\Services\Auth\SecurityEventRecorder; +use App\Services\Auth\TwoFactorChallenge; use Illuminate\Auth\Events\Registered; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; @@ -28,8 +30,11 @@ class RegisterController extends Controller /** * Handle an incoming registration request. */ - public function store(RegisterRequest $request): RedirectResponse - { + public function store( + RegisterRequest $request, + TwoFactorChallenge $twoFactor, + SecurityEventRecorder $securityEvents + ): RedirectResponse { $validated = $request->validated(); $user = User::create([ @@ -43,11 +48,22 @@ class RegisterController extends Controller event(new Registered($user)); Auth::login($user); + $request->session()->regenerate(); + $securityEvents->record('registration.completed', $user, $request); if (config('auth-ui.features.email_verification')) { return redirect()->route('verification.notice'); } + if ($twoFactor->enrollmentRequiredFor($user)) { + $request->session()->put( + 'url.intended', + config('auth-ui.redirects.register', '/') + ); + + return redirect()->route('two-factor.setup'); + } + return redirect(config('auth-ui.redirects.register', '/')); } } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index da73e31..c61059e 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -4,9 +4,13 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Http\Requests\Auth\ResetPasswordRequest; +use App\Models\User; +use App\Notifications\PasswordChangedNotification; +use App\Services\Auth\SecurityEventRecorder; use Illuminate\Auth\Events\PasswordReset; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Password; use Illuminate\Support\Str; use Inertia\Inertia; @@ -32,21 +36,39 @@ class ResetPasswordController extends Controller /** * Handle an incoming new password request. */ - public function store(ResetPasswordRequest $request): RedirectResponse - { + public function store( + ResetPasswordRequest $request, + SecurityEventRecorder $securityEvents + ): RedirectResponse { + $resetUser = null; $status = Password::reset( $request->validated(), - function ($user, string $password): void { + function (User $user, string $password) use ($request, &$resetUser): void { $user->forceFill([ 'password' => $password, 'remember_token' => Str::random(60), + 'auth_session_version' => $user->auth_session_version + 1, ])->save(); + if (config('session.driver') === 'database') { + DB::table(config('session.table')) + ->where('user_id', $user->getKey()) + ->delete(); + } + + $user->notify(new PasswordChangedNotification( + (string) $request->ip(), + now()->toIso8601String() + )); + $resetUser = $user; + event(new PasswordReset($user)); } ); if ($status === Password::PASSWORD_RESET) { + $securityEvents->record('password_reset.completed', $resetUser, $request); + return redirect()->route('login')->with('status', __($status)); } diff --git a/app/Http/Controllers/Auth/SocialiteController.php b/app/Http/Controllers/Auth/SocialiteController.php index 12344dc..71b2ec3 100644 --- a/app/Http/Controllers/Auth/SocialiteController.php +++ b/app/Http/Controllers/Auth/SocialiteController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Models\SocialAccount; use App\Models\User; +use App\Services\Auth\SecurityEventRecorder; use App\Services\Auth\TwoFactorChallenge; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -54,7 +55,8 @@ class SocialiteController extends Controller public function callback( Request $request, string $provider, - TwoFactorChallenge $twoFactor + TwoFactorChallenge $twoFactor, + SecurityEventRecorder $securityEvents ): RedirectResponse { if (! in_array($provider, $this->getEnabledProviders())) { abort(404, 'Provider not enabled'); @@ -74,7 +76,12 @@ class SocialiteController extends Controller ->first(); if ($socialAccount) { - return $this->finishAuthentication($request, $socialAccount->user, $twoFactor); + return $this->finishAuthentication( + $request, + $socialAccount->user, + $twoFactor, + $securityEvents + ); } // Check if a user with this email already exists @@ -92,7 +99,12 @@ class SocialiteController extends Controller 'provider_id' => $socialUser->getId(), ]); - return $this->finishAuthentication($request, $existingUser, $twoFactor); + return $this->finishAuthentication( + $request, + $existingUser, + $twoFactor, + $securityEvents + ); } // New user — check if registration is enabled @@ -134,20 +146,33 @@ class SocialiteController extends Controller 'provider_id' => $socialUser->getId(), ]); - return $this->finishAuthentication($request, $user, $twoFactor); + return $this->finishAuthentication($request, $user, $twoFactor, $securityEvents); } private function finishAuthentication( Request $request, User $user, - TwoFactorChallenge $twoFactor + TwoFactorChallenge $twoFactor, + SecurityEventRecorder $securityEvents ): RedirectResponse { if ($twoFactor->requiredFor($user)) { + $securityEvents->record('login.social_succeeded_pending_two_factor', $user, $request); + return $twoFactor->begin($request, $user, remember: true); } Auth::login($user, remember: true); $request->session()->regenerate(); + $securityEvents->record('login.social_succeeded', $user, $request); + + if ($twoFactor->enrollmentRequiredFor($user)) { + $request->session()->put( + 'url.intended', + config('auth-ui.redirects.login', '/') + ); + + return redirect()->route('two-factor.setup'); + } return redirect()->intended(config('auth-ui.redirects.login', '/')); } diff --git a/app/Http/Controllers/Auth/TwoFactorChallengeController.php b/app/Http/Controllers/Auth/TwoFactorChallengeController.php index e8bb9b6..1886ca5 100644 --- a/app/Http/Controllers/Auth/TwoFactorChallengeController.php +++ b/app/Http/Controllers/Auth/TwoFactorChallengeController.php @@ -3,6 +3,8 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Notifications\TwoFactorFailedNotification; +use App\Services\Auth\SecurityEventRecorder; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; @@ -19,7 +21,9 @@ class TwoFactorChallengeController extends Controller */ public function create(TwoFactorLoginRequest $request): Response|RedirectResponse { - if (! config('auth-ui.features.two_factor') || ! $request->hasChallengedUser()) { + if (! config('auth-ui.features.two_factor') + || ! $request->hasChallengedUser() + || $this->challengeHasExpired($request)) { $this->clearChallenge($request); return redirect()->route('login'); @@ -31,9 +35,13 @@ class TwoFactorChallengeController extends Controller /** * Complete the pending login using a TOTP or recovery code. */ - public function store(TwoFactorLoginRequest $request): RedirectResponse - { - if (! config('auth-ui.features.two_factor') || ! $request->hasChallengedUser()) { + public function store( + TwoFactorLoginRequest $request, + SecurityEventRecorder $securityEvents + ): RedirectResponse { + if (! config('auth-ui.features.two_factor') + || ! $request->hasChallengedUser() + || $this->challengeHasExpired($request)) { $this->clearChallenge($request); return redirect()->route('login'); @@ -51,6 +59,12 @@ class TwoFactorChallengeController extends Controller $user->replaceRecoveryCode($recoveryCode); } elseif (! $request->hasValidCode()) { event(new TwoFactorAuthenticationFailed($user)); + $user->notify(new TwoFactorFailedNotification( + (string) $request->ip(), + (string) $request->userAgent(), + now()->toIso8601String() + )); + $securityEvents->record('login.two_factor_failed', $user, $request); throw ValidationException::withMessages([ $request->filled('recovery_code') ? 'recovery_code' : 'code' => 'The provided authentication code was invalid.', @@ -61,12 +75,24 @@ class TwoFactorChallengeController extends Controller Auth::login($user, $request->remember()); $request->session()->regenerate(); + $securityEvents->record('login.two_factor_succeeded', $user, $request, [ + 'used_recovery_code' => (bool) $recoveryCode, + ]); return redirect()->intended(config('auth-ui.redirects.login', '/dashboard')); } private function clearChallenge(TwoFactorLoginRequest $request): void { - $request->session()->forget(['login.id', 'login.remember']); + $request->session()->forget(['login.id', 'login.remember', 'login.started_at']); + } + + private function challengeHasExpired(TwoFactorLoginRequest $request): bool + { + $startedAt = $request->session()->get('login.started_at'); + + return ! is_int($startedAt) + || now()->timestamp - $startedAt + > config('auth-ui.security.two_factor_challenge_timeout'); } } diff --git a/app/Http/Controllers/Auth/TwoFactorSettingsController.php b/app/Http/Controllers/Auth/TwoFactorSettingsController.php index 03504e5..00af30c 100644 --- a/app/Http/Controllers/Auth/TwoFactorSettingsController.php +++ b/app/Http/Controllers/Auth/TwoFactorSettingsController.php @@ -4,6 +4,8 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Models\User; +use App\Notifications\TwoFactorSecurityNotification; +use App\Services\Auth\SecurityEventRecorder; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -21,19 +23,19 @@ class TwoFactorSettingsController extends Controller */ public function enable(Request $request, EnableTwoFactorAuthentication $enable): RedirectResponse { - $this->ensureFeatureIsEnabled(); + $this->ensureFeatureIsEnabled($request); /** @var User $user */ $user = $request->user(); if ($user->hasEnabledTwoFactorAuthentication()) { - return redirect()->route('profile.show'); + return $this->enrollmentRedirect($request); } $this->validatePasswordForPasswordUser($request, $user); $enable($user, force: true); - return redirect()->route('profile.show') + return $this->enrollmentRedirect($request) ->with('success', 'Scan the QR code, then enter a code to finish enabling two-factor authentication.'); } @@ -42,9 +44,10 @@ class TwoFactorSettingsController extends Controller */ public function confirm( Request $request, - ConfirmTwoFactorAuthentication $confirm + ConfirmTwoFactorAuthentication $confirm, + SecurityEventRecorder $securityEvents ): RedirectResponse { - $this->ensureFeatureIsEnabled(); + $this->ensureFeatureIsEnabled($request); $validated = $request->validate([ 'code' => ['required', 'string', 'regex:/^\d{6}$/'], @@ -53,8 +56,18 @@ class TwoFactorSettingsController extends Controller /** @var User $user */ $user = $request->user(); $confirm($user, $validated['code']); + $user->notify(new TwoFactorSecurityNotification( + 'Two-factor authentication was enabled for your account.', + (string) $request->ip(), + now()->toIso8601String() + )); + $securityEvents->record('two_factor.enabled', $user, $request); - return redirect()->route('profile.show') + $redirect = $request->routeIs('two-factor.setup.confirm') + ? redirect()->route('two-factor.setup') + : redirect()->route('profile.show'); + + return $redirect ->with('success', 'Two-factor authentication is now enabled.') ->with('recoveryCodes', $user->fresh()->recoveryCodes()); } @@ -64,9 +77,15 @@ class TwoFactorSettingsController extends Controller */ public function disable( Request $request, - DisableTwoFactorAuthentication $disable + DisableTwoFactorAuthentication $disable, + SecurityEventRecorder $securityEvents ): RedirectResponse { - $this->ensureFeatureIsEnabled(); + $this->ensureFeatureIsEnabled($request); + abort_if( + config('auth-ui.features.two_factor_required'), + 403, + 'Two-factor authentication is required for this application.' + ); /** @var User $user */ $user = $request->user(); @@ -76,6 +95,12 @@ class TwoFactorSettingsController extends Controller } $disable($user); + $user->notify(new TwoFactorSecurityNotification( + 'Two-factor authentication was disabled for your account.', + (string) $request->ip(), + now()->toIso8601String() + )); + $securityEvents->record('two_factor.disabled', $user, $request); return redirect()->route('profile.show') ->with('success', 'Two-factor authentication has been disabled.'); @@ -86,9 +111,10 @@ class TwoFactorSettingsController extends Controller */ public function recoveryCodes( Request $request, - GenerateNewRecoveryCodes $generate + GenerateNewRecoveryCodes $generate, + SecurityEventRecorder $securityEvents ): RedirectResponse { - $this->ensureFeatureIsEnabled(); + $this->ensureFeatureIsEnabled($request); /** @var User $user */ $user = $request->user(); @@ -102,6 +128,14 @@ class TwoFactorSettingsController extends Controller if ($request->boolean('regenerate')) { $generate($user); $user->refresh(); + $user->notify(new TwoFactorSecurityNotification( + 'New two-factor authentication recovery codes were generated for your account.', + (string) $request->ip(), + now()->toIso8601String() + )); + $securityEvents->record('two_factor.recovery_codes_regenerated', $user, $request); + } else { + $securityEvents->record('two_factor.recovery_codes_revealed', $user, $request); } return redirect()->route('profile.show') @@ -111,9 +145,20 @@ class TwoFactorSettingsController extends Controller ->with('recoveryCodes', $user->recoveryCodes()); } - private function ensureFeatureIsEnabled(): void + private function ensureFeatureIsEnabled(Request $request): void { abort_unless(config('auth-ui.features.two_factor'), 404); + + if ($request->routeIs('two-factor.setup.*')) { + abort_unless(config('auth-ui.features.two_factor_required'), 404); + } + } + + private function enrollmentRedirect(Request $request): RedirectResponse + { + return $request->routeIs('two-factor.setup.*') + ? redirect()->route('two-factor.setup') + : redirect()->route('profile.show'); } private function validatePasswordForPasswordUser(Request $request, User $user): void @@ -131,8 +176,6 @@ class TwoFactorSettingsController extends Controller { if ($user->hasPassword()) { $this->validatePasswordForPasswordUser($request, $user); - - return; } $validated = $request->validate([ diff --git a/app/Http/Controllers/Auth/TwoFactorSetupController.php b/app/Http/Controllers/Auth/TwoFactorSetupController.php new file mode 100644 index 0000000..f5d9873 --- /dev/null +++ b/app/Http/Controllers/Auth/TwoFactorSetupController.php @@ -0,0 +1,54 @@ +user(); + + if ($user->hasEnabledTwoFactorAuthentication() + && ! $request->session()->has('recoveryCodes')) { + return redirect()->intended(config('auth-ui.redirects.login', '/')); + } + + return Inertia::render('Auth/TwoFactorSetup', [ + 'twoFactor' => $enrollmentState->for($user), + ]); + } + + /** + * Continue to the page that was requested before enrollment. + */ + public function complete(Request $request): RedirectResponse + { + abort_unless( + config('auth-ui.features.two_factor') + && $request->user()?->hasEnabledTwoFactorAuthentication(), + 404 + ); + + return redirect()->intended(config('auth-ui.redirects.login', '/')); + } +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 4be2b85..60b5043 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -3,38 +3,25 @@ namespace App\Http\Controllers; use App\Models\User; +use App\Services\Auth\TwoFactorEnrollmentState; use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; -use Laravel\Fortify\Fortify; class ProfileController extends Controller { /** * Display the authenticated user's profile and security settings. */ - public function __invoke(Request $request): Response - { + public function __invoke( + Request $request, + TwoFactorEnrollmentState $enrollmentState + ): Response { /** @var User $user */ $user = $request->user(); - $available = (bool) config('auth-ui.features.two_factor'); - $pending = $available - && filled($user->two_factor_secret) - && ! $user->hasEnabledTwoFactorAuthentication(); return Inertia::render('Profile/Show', [ - 'twoFactor' => [ - 'available' => $available, - 'enabled' => $available && $user->hasEnabledTwoFactorAuthentication(), - 'pending' => $pending, - 'requiresPassword' => $user->hasPassword(), - 'qrCodeDataUri' => $pending - ? 'data:image/svg+xml;base64,'.base64_encode($user->twoFactorQrCodeSvg()) - : null, - 'secretKey' => $pending - ? Fortify::currentEncrypter()->decrypt($user->two_factor_secret) - : null, - ], + 'twoFactor' => $enrollmentState->for($user), ]); } } diff --git a/app/Http/Middleware/AddSecurityHeaders.php b/app/Http/Middleware/AddSecurityHeaders.php new file mode 100644 index 0000000..22bf1b5 --- /dev/null +++ b/app/Http/Middleware/AddSecurityHeaders.php @@ -0,0 +1,58 @@ +headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('X-Frame-Options', 'DENY'); + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + $response->headers->set( + 'Permissions-Policy', + 'camera=(), geolocation=(), microphone=(), payment=(), usb=()' + ); + + if (app()->isProduction()) { + $response->headers->set( + 'Content-Security-Policy', + "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'" + ); + + if ($request->isSecure()) { + $response->headers->set( + 'Strict-Transport-Security', + 'max-age=63072000; includeSubDomains; preload' + ); + } + } + + if ($this->containsAuthenticationSecrets($request)) { + $response->headers->set( + 'Cache-Control', + 'no-store, no-cache, must-revalidate, private' + ); + $response->headers->set('Pragma', 'no-cache'); + $response->headers->set('Expires', '0'); + } + + return $response; + } + + private function containsAuthenticationSecrets(Request $request): bool + { + return $request->routeIs([ + 'login', + 'password.*', + 'two-factor.*', + 'profile.show', + ]); + } +} diff --git a/app/Http/Middleware/EnforceAbsoluteSessionTimeout.php b/app/Http/Middleware/EnforceAbsoluteSessionTimeout.php new file mode 100644 index 0000000..5483d1c --- /dev/null +++ b/app/Http/Middleware/EnforceAbsoluteSessionTimeout.php @@ -0,0 +1,82 @@ +user() || ! $request->hasSession()) { + return $next($request); + } + + /** @var User $user */ + $user = $request->user(); + $sessionVersion = $request->session()->get('auth.session_version'); + $userVersion = (int) $user->auth_session_version; + + if (! is_int($sessionVersion) && $userVersion === 0) { + $request->session()->put('auth.session_version', 0); + } elseif ($sessionVersion !== $userVersion) { + return $this->terminateSession( + $request, + $user, + 'session.revoked', + 'Your session is no longer valid. Please sign in again.' + ); + } + + $expiresAt = $request->session()->get('auth.absolute_expires_at'); + + if (! is_int($expiresAt)) { + $request->session()->put( + 'auth.absolute_expires_at', + now()->timestamp + config('auth-ui.security.absolute_session_lifetime') + ); + + return $next($request); + } + + if (now()->timestamp < $expiresAt) { + return $next($request); + } + + return $this->terminateSession( + $request, + $user, + 'session.absolute_timeout', + 'Your session has expired. Please sign in again.' + ); + } + + private function terminateSession( + Request $request, + User $user, + string $event, + string $message + ): Response { + $this->securityEvents->record($event, $user, $request); + + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + if ($request->expectsJson()) { + return response()->json(['message' => $message], 401); + } + + return redirect()->route('login') + ->with('error', $message); + } +} diff --git a/app/Http/Middleware/EnsureTwoFactorAuthenticationIsEnabled.php b/app/Http/Middleware/EnsureTwoFactorAuthenticationIsEnabled.php index 50b517a..95c95dc 100644 --- a/app/Http/Middleware/EnsureTwoFactorAuthenticationIsEnabled.php +++ b/app/Http/Middleware/EnsureTwoFactorAuthenticationIsEnabled.php @@ -24,18 +24,22 @@ class EnsureTwoFactorAuthenticationIsEnabled if ($request->expectsJson()) { return response()->json([ 'message' => 'Two-factor authentication setup is required.', - 'setup_url' => route('profile.show'), + 'setup_url' => route('two-factor.setup'), ], 403); } - return redirect()->route('profile.show') + if ($request->isMethod('GET')) { + $request->session()->put('url.intended', $request->fullUrl()); + } + + return redirect()->route('two-factor.setup') ->with('error', 'Two-factor authentication is required before you can continue.'); } private function isEnrollmentRoute(Request $request): bool { return $request->routeIs([ - 'profile.*', + 'two-factor.setup*', 'logout', 'verification.*', ]); diff --git a/app/Models/User.php b/app/Models/User.php index 8368616..c57833e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Notifications\QueuedResetPasswordNotification; use Database\Factories\UserFactory; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -61,6 +62,7 @@ class User extends Authenticatable implements MustVerifyEmail 'email_verified_at' => 'datetime', 'password' => 'hashed', 'two_factor_confirmed_at' => 'datetime', + 'auth_session_version' => 'integer', ]; } @@ -82,6 +84,15 @@ class User extends Authenticatable implements MustVerifyEmail return $this->password !== null; } + /** + * Queue password reset mail so the request does not reveal account + * existence through mail-delivery timing. + */ + public function sendPasswordResetNotification($token): void + { + $this->notify(new QueuedResetPasswordNotification($token)); + } + /** * Get the user's social accounts. */ diff --git a/app/Notifications/PasswordChangedNotification.php b/app/Notifications/PasswordChangedNotification.php new file mode 100644 index 0000000..b001ca5 --- /dev/null +++ b/app/Notifications/PasswordChangedNotification.php @@ -0,0 +1,38 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject('Your password was changed') + ->greeting('Hello '.$notifiable->first_name.',') + ->line('The password for your account was changed.') + ->line("Time: {$this->occurredAt}") + ->line("IP address: {$this->ipAddress}") + ->line('All other signed-in sessions have been ended.') + ->line('If you did not make this change, contact support immediately.'); + } +} diff --git a/app/Notifications/QueuedResetPasswordNotification.php b/app/Notifications/QueuedResetPasswordNotification.php new file mode 100644 index 0000000..24b6aa3 --- /dev/null +++ b/app/Notifications/QueuedResetPasswordNotification.php @@ -0,0 +1,12 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject('Failed two-factor authentication attempt') + ->greeting('Hello '.$notifiable->first_name.',') + ->line('A correct password was followed by a failed two-factor authentication attempt.') + ->line("Time: {$this->occurredAt}") + ->line("IP address: {$this->ipAddress}") + ->line("Browser: {$this->userAgent}") + ->line('If this was not you, change your password immediately.') + ->action('Review account security', url('/profile')); + } +} diff --git a/app/Notifications/TwoFactorSecurityNotification.php b/app/Notifications/TwoFactorSecurityNotification.php new file mode 100644 index 0000000..17238a7 --- /dev/null +++ b/app/Notifications/TwoFactorSecurityNotification.php @@ -0,0 +1,39 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage) + ->subject('Two-factor authentication security notice') + ->greeting('Hello '.$notifiable->first_name.',') + ->line($this->activity) + ->line("Time: {$this->occurredAt}") + ->line("IP address: {$this->ipAddress}") + ->line('If you did not perform this action, change your password and contact support immediately.') + ->action('Review account security', url('/profile')); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2ca7351..dcc45b4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,14 @@ namespace App\Providers; +use Illuminate\Auth\Events\Login; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Str; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Fortify; class AppServiceProvider extends ServiceProvider @@ -22,6 +29,73 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Event::listen(Login::class, function (Login $event): void { + if (request()->hasSession()) { + request()->session()->put([ + 'auth.session_version' => (int) $event->user->auth_session_version, + 'auth.absolute_expires_at' => now()->timestamp + + config('auth-ui.security.absolute_session_lifetime'), + ]); + } + }); + + Password::defaults(function (): Password { + return Password::min(15)->max(128); + }); + + RateLimiter::for('auth.login', function (Request $request): array { + $login = Str::lower((string) $request->input('login')); + + return [ + Limit::perMinute(20)->by('login-ip:'.$request->ip()), + Limit::perMinutes(10, 10)->by('login-account:'.hash('sha256', $login)), + ]; + }); + + RateLimiter::for('auth.two-factor', function (Request $request): array { + $userKey = $request->user()?->getAuthIdentifier() + ?? $request->session()->get('login.id') + ?? 'guest'; + + return [ + Limit::perMinute(20)->by('two-factor-ip:'.$request->ip()), + Limit::perMinutes(5, 5)->by('two-factor-account:'.$userKey), + ]; + }); + + RateLimiter::for('auth.password-email', function (Request $request): array { + $email = Str::lower((string) $request->input('email')); + + return [ + Limit::perMinutes(15, 20)->by('password-email-ip:'.$request->ip()), + Limit::perMinutes(15, 3)->by('password-email-account:'.hash('sha256', $email)), + ]; + }); + + RateLimiter::for('auth.password-reset', function (Request $request): array { + $email = Str::lower((string) $request->input('email')); + + return [ + Limit::perMinutes(15, 20)->by('password-reset-ip:'.$request->ip()), + Limit::perMinutes(15, 5)->by('password-reset-account:'.hash('sha256', $email)), + ]; + }); + + RateLimiter::for('auth.register', fn (Request $request): array => [ + Limit::perHour(20)->by('register-ip:'.$request->ip()), + ]); + + RateLimiter::for('auth.social', fn (Request $request): array => [ + Limit::perMinute(20)->by('social-ip:'.$request->ip()), + ]); + + RateLimiter::for('auth.account', function (Request $request): array { + $userKey = $request->user()?->getAuthIdentifier() ?? 'guest'; + + return [ + Limit::perMinute(20)->by('account-action-ip:'.$request->ip()), + Limit::perMinutes(5, 6)->by('account-action-user:'.$userKey), + ]; + }); } } diff --git a/app/Services/Auth/SecurityEventRecorder.php b/app/Services/Auth/SecurityEventRecorder.php new file mode 100644 index 0000000..38c9017 --- /dev/null +++ b/app/Services/Auth/SecurityEventRecorder.php @@ -0,0 +1,34 @@ + $context + */ + public function record( + string $event, + ?User $user, + Request $request, + array $context = [] + ): void { + Log::channel(config('auth-ui.security.log_channel'))->notice( + 'Authentication security event', + [ + 'event' => $event, + 'user_id' => $user?->getKey(), + 'ip_address' => $request->ip(), + 'user_agent' => Str::limit((string) $request->userAgent(), 255, ''), + ...$context, + ] + ); + } +} diff --git a/app/Services/Auth/TwoFactorChallenge.php b/app/Services/Auth/TwoFactorChallenge.php index d20d8cb..d7fceca 100644 --- a/app/Services/Auth/TwoFactorChallenge.php +++ b/app/Services/Auth/TwoFactorChallenge.php @@ -8,6 +8,16 @@ use Illuminate\Http\Request; class TwoFactorChallenge { + /** + * Determine whether the user must enroll in two-factor authentication. + */ + public function enrollmentRequiredFor(User $user): bool + { + return (bool) config('auth-ui.features.two_factor') + && (bool) config('auth-ui.features.two_factor_required') + && ! $user->hasEnabledTwoFactorAuthentication(); + } + /** * Determine whether the user must complete a two-factor challenge. */ @@ -25,6 +35,7 @@ class TwoFactorChallenge $request->session()->put([ 'login.id' => $user->getKey(), 'login.remember' => $remember, + 'login.started_at' => now()->timestamp, ]); $request->session()->regenerate(); diff --git a/app/Services/Auth/TwoFactorEnrollmentState.php b/app/Services/Auth/TwoFactorEnrollmentState.php new file mode 100644 index 0000000..d615b6c --- /dev/null +++ b/app/Services/Auth/TwoFactorEnrollmentState.php @@ -0,0 +1,43 @@ +hasEnabledTwoFactorAuthentication(); + $pending = $available + && filled($user->two_factor_secret) + && ! $enabled; + + return [ + 'available' => $available, + 'enabled' => $enabled, + 'pending' => $pending, + 'requiresPassword' => $user->hasPassword(), + 'qrCodeDataUri' => $pending + ? 'data:image/svg+xml;base64,'.base64_encode($user->twoFactorQrCodeSvg()) + : null, + 'secretKey' => $pending + ? Fortify::currentEncrypter()->decrypt($user->two_factor_secret) + : null, + ]; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index e1597f3..7edc0a1 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,7 @@ withMiddleware(function (Middleware $middleware): void { + $middleware->append(AddSecurityHeaders::class); + $middleware->web(append: [ + EnforceAbsoluteSessionTimeout::class, HandleInertiaRequests::class, EnsureTwoFactorAuthenticationIsEnabled::class, ]); diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..fc94ae6 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,5 +1,7 @@ =8.0,<8.0.15|>=8.1,<8.1.2", "symfony/translation-contracts": "<2.5", "symfony/var-dumper": "<8.1", "symfony/web-profiler-bundle": "<8.1", @@ -5396,7 +5397,7 @@ "symfony/validator": "^7.4|^8.0", "symfony/var-dumper": "^8.1", "symfony/var-exporter": "^7.4|^8.0", - "twig/twig": "^3.21" + "twig/twig": "^3.21|^4.0" }, "type": "library", "autoload": { @@ -5424,7 +5425,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v8.1.1" + "source": "https://github.com/symfony/http-kernel/tree/v8.1.2" }, "funding": [ { @@ -5444,20 +5445,20 @@ "type": "tidelift" } ], - "time": "2026-06-27T09:27:36+00:00" + "time": "2026-07-29T11:54:54+00:00" }, { "name": "symfony/mailer", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "4fa583a7377f28d54e4de442fba76375b2e20a12" + "reference": "221c7f326ace1ac2baee8331d829d5b7f04f4d53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/4fa583a7377f28d54e4de442fba76375b2e20a12", - "reference": "4fa583a7377f28d54e4de442fba76375b2e20a12", + "url": "https://api.github.com/repos/symfony/mailer/zipball/221c7f326ace1ac2baee8331d829d5b7f04f4d53", + "reference": "221c7f326ace1ac2baee8331d829d5b7f04f4d53", "shasum": "" }, "require": { @@ -5504,7 +5505,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v8.1.1" + "source": "https://github.com/symfony/mailer/tree/v8.1.2" }, "funding": [ { @@ -5524,20 +5525,20 @@ "type": "tidelift" } ], - "time": "2026-06-16T12:55:20+00:00" + "time": "2026-07-28T07:35:25+00:00" }, { "name": "symfony/mime", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664" + "reference": "75f4779d4ec2e13f24a3a7e5d0347c340c7ca627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b164ae7e3f7915aacfe9ee155f2f358502440664", - "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664", + "url": "https://api.github.com/repos/symfony/mime/zipball/75f4779d4ec2e13f24a3a7e5d0347c340c7ca627", + "reference": "75f4779d4ec2e13f24a3a7e5d0347c340c7ca627", "shasum": "" }, "require": { @@ -5590,7 +5591,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v8.1.0" + "source": "https://github.com/symfony/mime/tree/v8.1.2" }, "funding": [ { @@ -5610,7 +5611,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-29T08:00:47+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5697,16 +5698,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { @@ -5755,7 +5756,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -5775,7 +5776,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T05:58:03+00:00" + "time": "2026-07-28T08:25:59+00:00" }, { "name": "symfony/polyfill-intl-idn", @@ -6200,16 +6201,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { @@ -6256,7 +6257,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { @@ -6276,20 +6277,20 @@ "type": "tidelift" } ], - "time": "2026-05-26T02:25:22+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/polyfill-php86", - "version": "v1.38.0", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php86.git", - "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad" + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", - "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/6bc356ed3d8dbfeea8f0de235e34d670704e880e", + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e", "shasum": "" }, "require": { @@ -6336,7 +6337,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php86/tree/v1.38.0" + "source": "https://github.com/symfony/polyfill-php86/tree/v1.41.0" }, "funding": [ { @@ -6356,7 +6357,7 @@ "type": "tidelift" } ], - "time": "2026-05-25T11:52:35+00:00" + "time": "2026-07-02T13:42:24+00:00" }, { "name": "symfony/polyfill-uuid", @@ -6589,16 +6590,16 @@ }, { "name": "symfony/property-info", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7" + "reference": "289ef0d4f2b9bd5245ac2604564289a8673837b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/4721e8c56d0cd2378e0ef9a9899f810008b859f7", - "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7", + "url": "https://api.github.com/repos/symfony/property-info/zipball/289ef0d4f2b9bd5245ac2604564289a8673837b9", + "reference": "289ef0d4f2b9bd5245ac2604564289a8673837b9", "shasum": "" }, "require": { @@ -6651,7 +6652,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v8.1.0" + "source": "https://github.com/symfony/property-info/tree/v8.1.2" }, "funding": [ { @@ -6671,20 +6672,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "symfony/routing", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3" + "reference": "1058d4e13bb81dd9a6f7565686df7e13b880cdbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", - "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "url": "https://api.github.com/repos/symfony/routing/zipball/1058d4e13bb81dd9a6f7565686df7e13b880cdbd", + "reference": "1058d4e13bb81dd9a6f7565686df7e13b880cdbd", "shasum": "" }, "require": { @@ -6731,7 +6732,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v8.1.0" + "source": "https://github.com/symfony/routing/tree/v8.1.2" }, "funding": [ { @@ -6751,20 +6752,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "symfony/serializer", - "version": "v8.1.1", + "version": "v8.1.3", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "f911b744bc24658f435ea30439cfe536f0173a3a" + "reference": "6bd396438ba6c36800224e2beaf3f8f2d7439343" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/f911b744bc24658f435ea30439cfe536f0173a3a", - "reference": "f911b744bc24658f435ea30439cfe536f0173a3a", + "url": "https://api.github.com/repos/symfony/serializer/zipball/6bd396438ba6c36800224e2beaf3f8f2d7439343", + "reference": "6bd396438ba6c36800224e2beaf3f8f2d7439343", "shasum": "" }, "require": { @@ -6776,7 +6777,7 @@ "phpdocumentor/reflection-docblock": "<5.2|>=7", "phpdocumentor/type-resolver": "<1.5.1", "symfony/property-access": "<8.1", - "symfony/property-info": "<7.4", + "symfony/property-info": "<7.4.15", "symfony/type-info": "<7.4" }, "require-dev": { @@ -6795,7 +6796,7 @@ "symfony/messenger": "^7.4|^8.0", "symfony/mime": "^7.4|^8.0", "symfony/property-access": "^8.1", - "symfony/property-info": "^7.4|^8.0", + "symfony/property-info": "^7.4.15|~8.0.15|^8.1.2", "symfony/translation-contracts": "^2.5|^3", "symfony/type-info": "^7.4|^8.0", "symfony/uid": "^7.4|^8.0", @@ -6830,7 +6831,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v8.1.1" + "source": "https://github.com/symfony/serializer/tree/v8.1.3" }, "funding": [ { @@ -6850,7 +6851,7 @@ "type": "tidelift" } ], - "time": "2026-06-27T09:05:56+00:00" + "time": "2026-07-29T14:11:26+00:00" }, { "name": "symfony/service-contracts", @@ -6941,16 +6942,16 @@ }, { "name": "symfony/string", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", - "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", "shasum": "" }, "require": { @@ -7007,7 +7008,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.1.0" + "source": "https://github.com/symfony/string/tree/v8.1.2" }, "funding": [ { @@ -7027,7 +7028,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-28T07:35:25+00:00" }, { "name": "symfony/translation", @@ -7366,16 +7367,16 @@ }, { "name": "symfony/var-dumper", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "40096a2515a979f3125c5c928603995b8664c62a" + "reference": "865103cf742a039f34645b971fc3ace308d6c167" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/40096a2515a979f3125c5c928603995b8664c62a", - "reference": "40096a2515a979f3125c5c928603995b8664c62a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/865103cf742a039f34645b971fc3ace308d6c167", + "reference": "865103cf742a039f34645b971fc3ace308d6c167", "shasum": "" }, "require": { @@ -7391,7 +7392,7 @@ "symfony/http-kernel": "^7.4|^8.0", "symfony/process": "^7.4|^8.0", "symfony/uid": "^7.4|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "bin": [ "Resources/bin/var-dump-server" @@ -7429,7 +7430,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.1.1" + "source": "https://github.com/symfony/var-dumper/tree/v8.1.2" }, "funding": [ { @@ -7449,7 +7450,7 @@ "type": "tidelift" } ], - "time": "2026-06-09T10:54:51+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -7891,16 +7892,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.20.0", + "version": "v7.23.1", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" + "reference": "6b2aaf7d0e8d5220710d78921f28e5464a816596" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", - "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/6b2aaf7d0e8d5220710d78921f28e5464a816596", + "reference": "6b2aaf7d0e8d5220710d78921f28e5464a816596", "shasum": "" }, "require": { @@ -7910,25 +7911,25 @@ "ext-simplexml": "*", "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.3.0 || ~8.4.0 || ~8.5.0", - "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", - "phpunit/php-file-iterator": "^6.0.1 || ^7", - "phpunit/php-timer": "^8 || ^9", - "phpunit/phpunit": "^12.5.14 || ^13.0.5", - "sebastian/environment": "^8.0.3 || ^9", - "symfony/console": "^7.4.7 || ^8.0.7", - "symfony/process": "^7.4.5 || ^8.0.5" + "php": "~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^14.2.3", + "phpunit/php-file-iterator": "^7", + "phpunit/php-timer": "^9", + "phpunit/phpunit": "^13.2.5", + "sebastian/environment": "^9.3.2", + "symfony/console": "^7.4.8 || ^8.1.1", + "symfony/process": "^7.4.8 || ^8.1.0" }, "require-dev": { "doctrine/coding-standard": "^14.0.0", "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.44", - "phpstan/phpstan-deprecation-rules": "^2.0.4", - "phpstan/phpstan-phpunit": "^2.0.16", - "phpstan/phpstan-strict-rules": "^2.0.10", - "symfony/filesystem": "^7.4.6 || ^8.0.6" + "phpstan/phpstan": "^2.2.6", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.18", + "phpstan/phpstan-strict-rules": "^2.0.12", + "symfony/filesystem": "^7.4.8 || ^8.1.0" }, "bin": [ "bin/paratest", @@ -7968,7 +7969,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.23.1" }, "funding": [ { @@ -7980,149 +7981,7 @@ "type": "paypal" } ], - "time": "2026-03-29T15:46:14+00:00" - }, - { - "name": "composer/pcre", - "version": "3.4.0", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", - "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<2.2.2" - }, - "require-dev": { - "phpstan/phpstan": "^2", - "phpstan/phpstan-deprecation-rules": "^2", - "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "^9" - }, - "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.4.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2026-06-07T11:47:49+00:00" - }, - { - "name": "composer/xdebug-handler", - "version": "3.0.5", - "source": { - "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", - "shasum": "" - }, - "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" - }, - "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Composer\\XdebugHandler\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" - } - ], - "description": "Restarts a process without Xdebug.", - "keywords": [ - "Xdebug", - "performance" - ], - "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-05-06T16:37:16+00:00" + "time": "2026-07-28T13:47:02+00:00" }, { "name": "fakerphp/faker", @@ -8652,16 +8511,16 @@ }, { "name": "laravel/pint", - "version": "v1.29.3", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" + "reference": "72a0540d1aa10b6c146bda2a22f3ae003123c0ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", - "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "url": "https://api.github.com/repos/laravel/pint/zipball/72a0540d1aa10b6c146bda2a22f3ae003123c0ea", + "reference": "72a0540d1aa10b6c146bda2a22f3ae003123c0ea", "shasum": "" }, "require": { @@ -8672,14 +8531,16 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.95.8", - "illuminate/view": "^12.62.0", + "composer/semver": "^3.4.4", + "friendsofphp/php-cs-fixer": "^3.95.17", + "illuminate/view": "^12.64.0", "larastan/larastan": "^3.10.0", "laravel-zero/framework": "^12.1.0", "laravel/agent-detector": "^2.0.2", + "laravel/prompts": "^0.3.21", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.4.0", - "pestphp/pest": "^3.8.6" + "pestphp/pest": "^3.8.7" }, "bin": [ "builds/pint" @@ -8716,7 +8577,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-06-16T15:34:04+00:00" + "time": "2026-07-28T20:48:56+00:00" }, { "name": "laravel/roster", @@ -9083,42 +8944,43 @@ }, { "name": "pestphp/pest", - "version": "v4.7.5", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0" + "reference": "ae080becd6f036a2c83d9ebdd90079a06de118b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", - "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", + "url": "https://api.github.com/repos/pestphp/pest/zipball/ae080becd6f036a2c83d9ebdd90079a06de118b3", + "reference": "ae080becd6f036a2c83d9ebdd90079a06de118b3", "shasum": "" }, "require": { - "brianium/paratest": "^7.20.0", - "composer/xdebug-handler": "^3.0.5", - "nunomaduro/collision": "^8.9.4", + "brianium/paratest": "^7.23.1", + "nunomaduro/collision": "^8.9.5", "nunomaduro/termwind": "^2.4.0", - "pestphp/pest-plugin": "^4.0.0", - "pestphp/pest-plugin-arch": "^4.0.2", - "pestphp/pest-plugin-mutate": "^4.0.1", - "pestphp/pest-plugin-profanity": "^4.2.1", - "php": "^8.3.0", - "phpunit/phpunit": "^12.5.30", - "symfony/process": "^7.4.13|^8.1.0" + "pestphp/pest-plugin": "^5.0.0", + "pestphp/pest-plugin-arch": "^5.0.0", + "pestphp/pest-plugin-mutate": "^5.0.0", + "pestphp/pest-plugin-profanity": "^5.0.0", + "php": "^8.4", + "phpunit/phpunit": "^13.2.6", + "symfony/process": "^8.1.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.30", + "phpunit/phpunit": ">13.2.6", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "mrpunyapal/peststan": "^0.2.11", - "pestphp/pest-dev-tools": "^4.1.0", - "pestphp/pest-plugin-browser": "^4.3.1", - "pestphp/pest-plugin-type-coverage": "^4.0.4", + "laravel/pao": "^1.1.3", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-browser": "^5.0.0", + "pestphp/pest-plugin-phpstan": "^5.0.0", + "pestphp/pest-plugin-rector": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.0", "psy/psysh": "^0.12.24" }, "bin": [ @@ -9186,7 +9048,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.7.5" + "source": "https://github.com/pestphp/pest/tree/v5.0.2" }, "funding": [ { @@ -9198,34 +9060,34 @@ "type": "github" } ], - "time": "2026-07-06T17:06:29+00:00" + "time": "2026-07-29T19:27:11+00:00" }, { "name": "pestphp/pest-plugin", - "version": "v4.0.0", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + "reference": "87283f41aafa2561b7618be53eab00f2d06f4140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", - "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/87283f41aafa2561b7618be53eab00f2d06f4140", + "reference": "87283f41aafa2561b7618be53eab00f2d06f4140", "shasum": "" }, "require": { "composer-plugin-api": "^2.0.0", "composer-runtime-api": "^2.2.2", - "php": "^8.3" + "php": "^8.4" }, "conflict": { - "pestphp/pest": "<4.0.0" + "pestphp/pest": "<5.0.0" }, "require-dev": { - "composer/composer": "^2.8.10", - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0" + "composer/composer": "^2.10.2", + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "composer-plugin", "extra": { @@ -9252,7 +9114,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + "source": "https://github.com/pestphp/pest-plugin/tree/v5.0.0" }, "funding": [ { @@ -9268,30 +9130,33 @@ "type": "patreon" } ], - "time": "2025-08-20T12:35:58+00:00" + "time": "2026-07-18T17:10:45+00:00" }, { "name": "pestphp/pest-plugin-arch", - "version": "v4.0.2", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" + "reference": "244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", - "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1", + "reference": "244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1", "shasum": "" }, "require": { - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", "ta-tikoma/phpunit-architecture-test": "^0.8.7" }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, "require-dev": { - "pestphp/pest": "^4.4.6", - "pestphp/pest-dev-tools": "^4.1.0" + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "library", "extra": { @@ -9326,7 +9191,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v5.0.0" }, "funding": [ { @@ -9338,32 +9203,35 @@ "type": "github" } ], - "time": "2026-04-10T17:20:19+00:00" + "time": "2026-07-21T07:48:55+00:00" }, { "name": "pestphp/pest-plugin-mutate", - "version": "v4.0.1", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-mutate.git", - "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + "reference": "fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", - "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77", + "reference": "fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77", "shasum": "" }, "require": { - "nikic/php-parser": "^5.6.1", - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3", + "nikic/php-parser": "^5.8.0", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", "psr/simple-cache": "^3.0.0" }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, "require-dev": { - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0", - "pestphp/pest-plugin-type-coverage": "^4.0.0" + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.0" }, "type": "library", "autoload": { @@ -9398,7 +9266,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v5.0.0" }, "funding": [ { @@ -9414,30 +9282,33 @@ "type": "github" } ], - "time": "2025-08-21T20:19:25+00:00" + "time": "2026-07-21T07:48:56+00:00" }, { "name": "pestphp/pest-plugin-profanity", - "version": "v4.2.1", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-profanity.git", - "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + "reference": "3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", - "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9", + "reference": "3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9", "shasum": "" }, "require": { - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3" + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4" + }, + "conflict": { + "pestphp/pest": "<5.0.0" }, "require-dev": { - "faissaloux/pest-plugin-inside": "^1.9", - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0" + "faissaloux/pest-plugin-inside": "^1.11", + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "library", "extra": { @@ -9468,9 +9339,9 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v5.0.0" }, - "time": "2025-12-08T00:13:17+00:00" + "time": "2026-07-21T07:48:56+00:00" }, { "name": "phar-io/manifest", @@ -9592,33 +9463,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.7", + "version": "14.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "186dab580576598076de6818596d12b61801880e" + "reference": "048a5c12bdb4580f4767ce2761793a16b170fbe4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", - "reference": "186dab580576598076de6818596d12b61801880e", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/048a5c12bdb4580f4767ce2761793a16b170fbe4", + "reference": "048a5c12bdb4580f4767ce2761793a16b170fbe4", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", + "ext-mbstring": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.7.0", - "php": ">=8.3", - "phpunit/php-text-template": "^5.0", - "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.1.2", - "sebastian/lines-of-code": "^4.0.1", - "sebastian/version": "^6.0", + "nikic/php-parser": "^5.8.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.1", + "sebastian/version": "^7.0", "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^13.2.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -9627,7 +9500,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5.x-dev" + "dev-main": "14.2.x-dev" } }, "autoload": { @@ -9656,7 +9529,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.2.4" }, "funding": [ { @@ -9676,32 +9549,32 @@ "type": "tidelift" } ], - "time": "2026-06-01T13:24:19+00:00" + "time": "2026-07-30T17:01:07+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "6.0.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -9729,7 +9602,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.0" }, "funding": [ { @@ -9749,28 +9622,28 @@ "type": "tidelift" } ], - "time": "2026-02-02T14:04:18+00:00" + "time": "2026-02-06T04:33:26+00:00" }, { "name": "phpunit/php-invoker", - "version": "6.0.0", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "suggest": { "ext-pcntl": "*" @@ -9778,7 +9651,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -9805,40 +9678,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:58+00:00" + "time": "2026-02-06T04:34:47+00:00" }, { "name": "phpunit/php-text-template", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -9865,40 +9750,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:16+00:00" + "time": "2026-02-06T04:36:37+00:00" }, { "name": "phpunit/php-timer", - "version": "8.0.0", + "version": "9.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -9925,56 +9822,70 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:38+00:00" + "time": "2026-02-06T04:37:53+00:00" }, { "name": "phpunit/phpunit", - "version": "12.5.30", + "version": "13.2.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb" + "reference": "5d2afe181339a56348ef9a80fa7eb806b7eae508" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/900400a5b616d6fb306f9549f6da33ba615d3fbb", - "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5d2afe181339a56348ef9a80fa7eb806b7eae508", + "reference": "5d2afe181339a56348ef9a80fa7eb806b7eae508", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.7", - "phpunit/php-file-iterator": "^6.0.1", - "phpunit/php-invoker": "^6.0.0", - "phpunit/php-text-template": "^5.0.0", - "phpunit/php-timer": "^8.0.0", - "sebastian/cli-parser": "^4.2.1", - "sebastian/comparator": "^7.1.8", - "sebastian/diff": "^7.0.0", - "sebastian/environment": "^8.1.2", - "sebastian/exporter": "^7.0.3", - "sebastian/global-state": "^8.0.3", - "sebastian/object-enumerator": "^7.0.0", - "sebastian/recursion-context": "^7.0.1", - "sebastian/type": "^6.0.4", - "sebastian/version": "^6.0.0", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.2.3", + "phpunit/php-file-iterator": "^7.0.0", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.0", + "sebastian/comparator": "^8.3.0", + "sebastian/diff": "^9.0", + "sebastian/environment": "^9.3.2", + "sebastian/exporter": "^8.1.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.0.0", + "sebastian/recursion-context": "^8.0.0", + "sebastian/type": "^7.0.1", + "sebastian/version": "^7.0.0", "staabm/side-effects-detector": "^1.0.5" }, "bin": [ @@ -9983,7 +9894,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5-dev" + "dev-main": "13.2-dev" } }, "autoload": { @@ -10015,7 +9926,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.30" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.6" }, "funding": [ { @@ -10023,32 +9934,32 @@ "type": "other" } ], - "time": "2026-06-15T13:12:30+00:00" + "time": "2026-07-28T14:00:09+00:00" }, { "name": "sebastian/cli-parser", - "version": "4.2.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", - "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/48a4654fa5e48c1c81214e9930048a572d4b23ca", + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.2-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -10072,7 +9983,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.0" }, "funding": [ { @@ -10092,31 +10003,31 @@ "type": "tidelift" } ], - "time": "2026-05-17T05:29:34+00:00" + "time": "2026-02-06T04:39:44+00:00" }, { "name": "sebastian/comparator", - "version": "7.1.8", + "version": "8.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "7c65c1e79836812819705b473a90c12399542485" + "reference": "c025fc7604afab3f195fab7cdaf72327331af241" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", - "reference": "7c65c1e79836812819705b473a90c12399542485", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c025fc7604afab3f195fab7cdaf72327331af241", + "reference": "c025fc7604afab3f195fab7cdaf72327331af241", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/diff": "^7.0", - "sebastian/exporter": "^7.0.3" + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.1.0" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -10124,7 +10035,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "7.1-dev" + "dev-main": "8.3-dev" } }, "autoload": { @@ -10164,7 +10075,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/8.3.0" }, "funding": [ { @@ -10184,33 +10095,33 @@ "type": "tidelift" } ], - "time": "2026-05-21T04:45:25+00:00" + "time": "2026-06-05T03:06:45+00:00" }, { "name": "sebastian/complexity", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -10234,41 +10145,53 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:25+00:00" + "time": "2026-02-06T04:41:32+00:00" }, { "name": "sebastian/diff", - "version": "7.0.0", + "version": "9.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + "reference": "a3fb6a298a265ff487a91bbea46e03cd01dbb226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a3fb6a298a265ff487a91bbea46e03cd01dbb226", + "reference": "a3fb6a298a265ff487a91bbea46e03cd01dbb226", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^13.2", + "symfony/process": "^7.4.13" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -10301,35 +10224,47 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:46+00:00" + "time": "2026-06-05T03:04:51+00:00" }, { "name": "sebastian/environment", - "version": "8.1.2", + "version": "9.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", - "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.26" + "phpunit/phpunit": "^13.1.11" }, "suggest": { "ext-posix": "*" @@ -10337,7 +10272,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.1-dev" + "dev-main": "9.3-dev" } }, "autoload": { @@ -10365,7 +10300,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.2" }, "funding": [ { @@ -10385,34 +10320,34 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:40:20+00:00" + "time": "2026-05-25T13:41:38+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.3", + "version": "8.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + "reference": "cfaa77c750dcad6f44c9bac8f62ac486e1c82c26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", - "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/cfaa77c750dcad6f44c9bac8f62ac486e1c82c26", + "reference": "cfaa77c750dcad6f44c9bac8f62ac486e1c82c26", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/recursion-context": "^7.0.1" + "php": ">=8.4", + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -10455,7 +10390,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/8.1.1" }, "funding": [ { @@ -10475,35 +10410,173 @@ "type": "tidelift" } ], - "time": "2026-05-20T04:37:17+00:00" + "time": "2026-07-13T11:35:11+00:00" }, { - "name": "sebastian/global-state", - "version": "8.0.3", + "name": "sebastian/file-filter", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", - "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0.1" + "php": ">=8.4" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/git-state", + "type": "tidelift" + } + ], + "time": "2026-03-21T12:54:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^13.1.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" } }, "autoload": { @@ -10529,7 +10602,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.1" }, "funding": [ { @@ -10549,33 +10622,33 @@ "type": "tidelift" } ], - "time": "2026-06-01T15:10:33+00:00" + "time": "2026-06-01T15:11:33+00:00" }, { "name": "sebastian/lines-of-code", - "version": "4.0.1", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", - "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", "shasum": "" }, "require": { - "nikic/php-parser": "^5.7.0", - "php": ">=8.3" + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -10599,7 +10672,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.2" }, "funding": [ { @@ -10619,34 +10692,34 @@ "type": "tidelift" } ], - "time": "2026-05-19T16:22:07+00:00" + "time": "2026-07-09T08:42:34+00:00" }, { "name": "sebastian/object-enumerator", - "version": "7.0.0", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10669,40 +10742,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", + "type": "tidelift" } ], - "time": "2025-02-07T04:57:48+00:00" + "time": "2026-02-06T04:46:36+00:00" }, { "name": "sebastian/object-reflector", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -10725,40 +10810,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:17+00:00" + "time": "2026-02-06T04:47:13+00:00" }, { "name": "sebastian/recursion-context", - "version": "7.0.1", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/74c5af21f6a5833e91767ca068c4d3dfec15317e", + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10789,7 +10886,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.0" }, "funding": [ { @@ -10809,32 +10906,32 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:44:59+00:00" + "time": "2026-02-06T04:51:28+00:00" }, { "name": "sebastian/type", - "version": "6.0.4", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "82ff822c2edc46724be9f7411d3163021f602773" + "reference": "fee0309275847fefd7636167085e379c1dbf6990" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", - "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fee0309275847fefd7636167085e379c1dbf6990", + "reference": "fee0309275847fefd7636167085e379c1dbf6990", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.1.10" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10858,7 +10955,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + "source": "https://github.com/sebastianbergmann/type/tree/7.0.1" }, "funding": [ { @@ -10878,29 +10975,29 @@ "type": "tidelift" } ], - "time": "2026-05-20T06:45:45+00:00" + "time": "2026-05-20T06:49:11+00:00" }, { "name": "sebastian/version", - "version": "6.0.0", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10924,15 +11021,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:38+00:00" + "time": "2026-02-06T04:52:52+00:00" }, { "name": "staabm/side-effects-detector", @@ -10988,16 +11097,16 @@ }, { "name": "symfony/yaml", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "8e4cdd4311683516be06944f4b85244063cdb886" + "reference": "faabdbe998e8c5c599dceffa27aa265b185c0736" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/8e4cdd4311683516be06944f4b85244063cdb886", - "reference": "8e4cdd4311683516be06944f4b85244063cdb886", + "url": "https://api.github.com/repos/symfony/yaml/zipball/faabdbe998e8c5c599dceffa27aa265b185c0736", + "reference": "faabdbe998e8c5c599dceffa27aa265b185c0736", "shasum": "" }, "require": { @@ -11040,7 +11149,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.1.1" + "source": "https://github.com/symfony/yaml/tree/v8.1.2" }, "funding": [ { @@ -11060,7 +11169,7 @@ "type": "tidelift" } ], - "time": "2026-06-09T11:06:24+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", diff --git a/config/auth-ui.php b/config/auth-ui.php index 53a50f2..7aa2f46 100644 --- a/config/auth-ui.php +++ b/config/auth-ui.php @@ -31,6 +31,12 @@ return [ 'two_factor_required' => env('AUTH_REQUIRE_TWO_FACTOR', false), ], + 'security' => [ + 'log_channel' => env('AUTH_SECURITY_LOG_CHANNEL', env('LOG_CHANNEL', 'stack')), + 'two_factor_challenge_timeout' => (int) env('AUTH_TWO_FACTOR_CHALLENGE_TIMEOUT', 300), + 'absolute_session_lifetime' => (int) env('AUTH_ABSOLUTE_SESSION_LIFETIME', 28800), + ], + /* |-------------------------------------------------------------------------- | Login Page Configuration diff --git a/config/auth.php b/config/auth.php index 7d1eb0d..d7568ff 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,5 +1,7 @@ [ 'users' => [ 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', App\Models\User::class), + 'model' => env('AUTH_MODEL', User::class), ], // 'users' => [ diff --git a/config/database.php b/config/database.php index c57fa63..93b15be 100644 --- a/config/database.php +++ b/config/database.php @@ -1,6 +1,7 @@ true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -79,7 +80,7 @@ return [ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], diff --git a/config/fortify.php b/config/fortify.php index 9d3e8f6..81af56a 100644 --- a/config/fortify.php +++ b/config/fortify.php @@ -11,6 +11,8 @@ return [ ? [ Features::twoFactorAuthentication([ 'confirm' => true, + 'secret-length' => 32, + 'window' => 0, ]), ] : [], diff --git a/config/hashing.php b/config/hashing.php new file mode 100644 index 0000000..e9ea2f4 --- /dev/null +++ b/config/hashing.php @@ -0,0 +1,30 @@ + env('HASH_DRIVER', 'argon2id'), + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 12), + 'verify' => env('HASH_VERIFY', true), + 'limit' => env('BCRYPT_LIMIT', null), + ], + + 'argon' => [ + 'memory' => env('ARGON_MEMORY', 65536), + 'threads' => env('ARGON_THREADS', 1), + 'time' => env('ARGON_TIME', 4), + 'verify' => env('HASH_VERIFY', true), + ], + + 'rehash_on_login' => true, +]; diff --git a/config/session.php b/config/session.php index bd3ec34..1101d44 100644 --- a/config/session.php +++ b/config/session.php @@ -47,7 +47,9 @@ return [ | */ - 'encrypt' => env('SESSION_ENCRYPT', false), + 'encrypt' => env('APP_ENV') === 'production' + ? true + : env('SESSION_ENCRYPT', false), /* |-------------------------------------------------------------------------- @@ -169,7 +171,9 @@ return [ | */ - 'secure' => env('SESSION_SECURE_COOKIE'), + 'secure' => env('APP_ENV') === 'production' + ? true + : env('SESSION_SECURE_COOKIE', false), /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2026_07_31_000000_add_auth_session_version_to_users_table.php b/database/migrations/2026_07_31_000000_add_auth_session_version_to_users_table.php new file mode 100644 index 0000000..0c201a0 --- /dev/null +++ b/database/migrations/2026_07_31_000000_add_auth_session_version_to_users_table.php @@ -0,0 +1,22 @@ +unsignedInteger('auth_session_version')->default(0); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropColumn('auth_session_version'); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index 1a9795b..2adbfca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "html", "dependencies": { "@inertiajs/vue3": "^3.6.1", "@nuxt/ui": "^4.10.0", @@ -16,13 +15,13 @@ "@tailwindcss/vite": "^4.3.3", "@vitejs/plugin-vue": "^6.0.8", "@vue/test-utils": "^2.4.11", - "concurrently": "^10.0.3", - "eslint": "^10.7.0", + "concurrently": "^10.0.4", + "eslint": "^10.8.0", "jsdom": "^29.1.1", "laravel-vite-plugin": "^3.1.3", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", - "vite": "^8.1.5", + "vite": "^8.2.0", "vitest": "^4.1.10" } }, @@ -542,9 +541,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -558,9 +557,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -574,9 +573,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -590,9 +589,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -606,9 +605,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -622,9 +621,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -638,9 +637,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -654,9 +653,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -670,9 +669,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -686,9 +685,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -702,9 +701,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -718,9 +717,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -734,9 +733,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -750,9 +749,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -766,9 +765,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -782,9 +781,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -798,9 +797,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -814,9 +813,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -830,9 +829,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -846,9 +845,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -862,9 +861,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -878,9 +877,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -894,9 +893,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -910,9 +909,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -926,9 +925,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -942,9 +941,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1055,16 +1054,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", @@ -1095,9 +1084,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1415,109 +1404,6 @@ "@swc/helpers": "^0.5.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1564,21 +1450,24 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz", + "integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==", "license": "MIT", "optional": true, "dependencies": { "@tybys/wasm-util": "^0.10.3" }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "node_modules/@nuxt/devtools-kit": { @@ -1924,9 +1813,9 @@ } }, "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.2.1.tgz", + "integrity": "sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==", "dev": true, "license": "MIT" }, @@ -1944,25 +1833,14 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@pkgr/core": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", @@ -1983,9 +1861,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", + "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", "cpu": [ "arm64" ], @@ -1999,9 +1877,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", + "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", "cpu": [ "arm64" ], @@ -2015,9 +1893,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", + "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", "cpu": [ "x64" ], @@ -2031,9 +1909,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", + "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", "cpu": [ "x64" ], @@ -2047,9 +1925,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", + "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", "cpu": [ "arm" ], @@ -2063,9 +1941,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", + "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", "cpu": [ "arm64" ], @@ -2082,9 +1960,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", + "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", "cpu": [ "arm64" ], @@ -2101,9 +1979,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", + "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", "cpu": [ "ppc64" ], @@ -2120,9 +1998,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", + "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", "cpu": [ "s390x" ], @@ -2139,9 +2017,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", + "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", "cpu": [ "x64" ], @@ -2158,9 +2036,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", + "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", "cpu": [ "x64" ], @@ -2177,9 +2055,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", + "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", "cpu": [ "arm64" ], @@ -2193,27 +2071,55 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", + "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" + "@emnapi/core": "2.0.0-alpha.3", + "@emnapi/runtime": "2.0.0-alpha.3", + "@napi-rs/wasm-runtime": "^1.2.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", + "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", "cpu": [ "arm64" ], @@ -2227,9 +2133,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", + "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", "cpu": [ "x64" ], @@ -3501,16 +3407,6 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", @@ -4008,13 +3904,13 @@ } }, "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-5.0.0.tgz", + "integrity": "sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==", "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/acorn": { @@ -4056,32 +3952,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/ansis": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", @@ -4150,11 +4020,14 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/baseline-browser-mapping": { "version": "2.11.1", @@ -4187,13 +4060,16 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/browserslist": { @@ -4473,26 +4349,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/colortranslator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/colortranslator/-/colortranslator-5.0.0.tgz", @@ -4500,13 +4356,13 @@ "license": "Apache-2.0" }, "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/comment-parser": { @@ -4520,15 +4376,15 @@ } }, "node_modules/concurrently": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", - "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.4.tgz", + "integrity": "sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==", "dev": true, "license": "MIT", "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", - "shell-quote": "1.8.4", + "shell-quote": "1.9.0", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" @@ -4805,30 +4661,23 @@ "url": "https://dotenvx.com" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/editorconfig": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", - "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-3.0.2.tgz", + "integrity": "sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==", "dev": true, "license": "MIT", "dependencies": { - "@one-ini/wasm": "0.1.1", - "commander": "^10.0.0", - "minimatch": "^9.0.1", - "semver": "^7.5.3" + "@one-ini/wasm": "0.2.1", + "commander": "^14.0.3", + "minimatch": "~10.2.4", + "semver": "^7.7.4" }, "bin": { "editorconfig": "bin/editorconfig" }, "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/electron-to-chromium": { @@ -4926,13 +4775,6 @@ "embla-carousel": "^8.0.0 || ~8.0.0-rc03" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/empathic": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", @@ -4993,9 +4835,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -5005,32 +4847,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -5057,9 +4899,9 @@ } }, "node_modules/eslint": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", - "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -5069,7 +4911,7 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", + "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", @@ -5093,7 +4935,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -5732,29 +5574,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", @@ -5796,22 +5615,6 @@ "node": ">= 4" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -6141,23 +5944,6 @@ "node": ">=18.12.0" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -6299,21 +6085,18 @@ "license": "ISC" }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6526,16 +6309,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6602,22 +6375,6 @@ "url": "https://github.com/sponsors/dmonad" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -6628,17 +6385,17 @@ } }, "node_modules/js-beautify": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", - "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-2.0.3.tgz", + "integrity": "sha512-cyFbh3tkPhknnTD/0bLf0T0yy2ZIbqL05mttzbt4y1Zfr7NxqXQZ62dkBLKs3oHH/lpjmDRAnciJiSUyOy8XwQ==", "dev": true, "license": "MIT", "dependencies": { "config-chain": "^1.1.13", - "editorconfig": "^1.0.4", - "glob": "^10.4.2", - "js-cookie": "^3.0.5", - "nopt": "^7.2.1" + "editorconfig": "^3.0.2", + "glob": "^13.0.6", + "js-cookie": "^3.0.8", + "nopt": "^10.0.1" }, "bin": { "css-beautify": "js/bin/css-beautify.js", @@ -6713,16 +6470,6 @@ } } }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7232,11 +6979,13 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/magic-regexp": { "version": "0.10.0", @@ -8221,27 +7970,27 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -8387,19 +8136,19 @@ } }, "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-10.0.1.tgz", + "integrity": "sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^2.0.0" + "abbrev": "^5.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/normalize-path": { @@ -8588,13 +8337,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/package-manager-detector": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", @@ -8674,17 +8416,17 @@ } }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -8762,9 +8504,9 @@ } }, "node_modules/postcss": { - "version": "8.5.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", - "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "funding": [ { "type": "opencollective", @@ -9125,12 +8867,12 @@ } }, "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", + "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", + "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -9140,21 +8882,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "@rolldown/binding-android-arm64": "1.2.1", + "@rolldown/binding-darwin-arm64": "1.2.1", + "@rolldown/binding-darwin-x64": "1.2.1", + "@rolldown/binding-freebsd-x64": "1.2.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", + "@rolldown/binding-linux-arm64-gnu": "1.2.1", + "@rolldown/binding-linux-arm64-musl": "1.2.1", + "@rolldown/binding-linux-ppc64-gnu": "1.2.1", + "@rolldown/binding-linux-s390x-gnu": "1.2.1", + "@rolldown/binding-linux-x64-gnu": "1.2.1", + "@rolldown/binding-linux-x64-musl": "1.2.1", + "@rolldown/binding-openharmony-arm64": "1.2.1", + "@rolldown/binding-wasm32-wasi": "1.2.1", + "@rolldown/binding-win32-arm64-msvc": "1.2.1", + "@rolldown/binding-win32-x64-msvc": "1.2.1" } }, "node_modules/rope-sequence": { @@ -9241,9 +8983,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "dev": true, "license": "MIT", "engines": { @@ -9340,64 +9082,6 @@ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -10149,15 +9833,6 @@ } } }, - "node_modules/unstorage/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/untyped": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz", @@ -10345,15 +10020,15 @@ } }, "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", + "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "bin": { @@ -10370,7 +10045,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -10445,6 +10120,267 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", @@ -10722,25 +10658,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", diff --git a/package.json b/package.json index 7499a86..f2849da 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,19 @@ "@tailwindcss/vite": "^4.3.3", "@vitejs/plugin-vue": "^6.0.8", "@vue/test-utils": "^2.4.11", - "concurrently": "^10.0.3", - "eslint": "^10.7.0", + "concurrently": "^10.0.4", + "eslint": "^10.8.0", "jsdom": "^29.1.1", "laravel-vite-plugin": "^3.1.3", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", - "vite": "^8.1.5", + "vite": "^8.2.0", "vitest": "^4.1.10" + }, + "overrides": { + "@vue/test-utils": { + "js-beautify": "2.0.3" + }, + "esbuild": "0.28.1" } } diff --git a/phpunit.xml b/phpunit.xml index bbfcf81..64d2fe6 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -20,7 +20,11 @@ - + + + + + diff --git a/resources/js/Pages/Auth/TwoFactorChallenge.vue b/resources/js/Pages/Auth/TwoFactorChallenge.vue index 78d538d..a3fa74c 100644 --- a/resources/js/Pages/Auth/TwoFactorChallenge.vue +++ b/resources/js/Pages/Auth/TwoFactorChallenge.vue @@ -26,7 +26,7 @@ function submit() {
- +

Two-factor authentication

diff --git a/resources/js/Pages/Auth/TwoFactorSetup.vue b/resources/js/Pages/Auth/TwoFactorSetup.vue new file mode 100644 index 0000000..9bbf360 --- /dev/null +++ b/resources/js/Pages/Auth/TwoFactorSetup.vue @@ -0,0 +1,222 @@ + + + diff --git a/resources/js/Pages/Profile/Show.vue b/resources/js/Pages/Profile/Show.vue index 5e1cf6e..d0a6093 100644 --- a/resources/js/Pages/Profile/Show.vue +++ b/resources/js/Pages/Profile/Show.vue @@ -1,6 +1,7 @@ + + diff --git a/resources/js/validation/__tests__/auth.test.ts b/resources/js/validation/__tests__/auth.test.ts index 497ba2f..d2cdd3f 100644 --- a/resources/js/validation/__tests__/auth.test.ts +++ b/resources/js/validation/__tests__/auth.test.ts @@ -40,8 +40,8 @@ describe('registerSchema', () => { first_name: 'Test', last_name: 'User', email: 'test@example.com', - password: 'password123', - password_confirmation: 'password123', + password: 'correct horse battery staple', + password_confirmation: 'correct horse battery staple', } it('accepts valid registration data', () => { @@ -69,8 +69,8 @@ describe('registerSchema', () => { expect(result.success).toBe(false) }) - it('rejects password shorter than 8 characters', () => { - const result = validate(registerSchema, { ...validData, password: 'short', password_confirmation: 'short' }) + it('rejects password shorter than 15 characters', () => { + const result = validate(registerSchema, { ...validData, password: 'too-short', password_confirmation: 'too-short' }) expect(result.success).toBe(false) }) @@ -110,8 +110,8 @@ describe('forgotPasswordSchema', () => { describe('resetPasswordSchema', () => { const validData = { email: 'test@example.com', - password: 'newpassword123', - password_confirmation: 'newpassword123', + password: 'a secure new password', + password_confirmation: 'a secure new password', } it('accepts valid reset data', () => { @@ -124,8 +124,8 @@ describe('resetPasswordSchema', () => { expect(result.success).toBe(false) }) - it('rejects password shorter than 8 characters', () => { - const result = validate(resetPasswordSchema, { ...validData, password: 'short', password_confirmation: 'short' }) + it('rejects password shorter than 15 characters', () => { + const result = validate(resetPasswordSchema, { ...validData, password: 'too-short', password_confirmation: 'too-short' }) expect(result.success).toBe(false) }) diff --git a/resources/js/validation/auth.ts b/resources/js/validation/auth.ts index f17dee5..a959eb1 100644 --- a/resources/js/validation/auth.ts +++ b/resources/js/validation/auth.ts @@ -17,7 +17,7 @@ export const registerSchema = v.pipe( first_name: v.pipe(v.string('First name is required'), v.nonEmpty('First name is required')), last_name: v.pipe(v.string('Last name is required'), v.nonEmpty('Last name is required')), email: v.pipe(v.string('Email is required'), v.nonEmpty('Email is required'), v.email('Please enter a valid email')), - password: v.pipe(v.string('Password is required'), v.nonEmpty('Password is required'), v.minLength(8, 'Password must be at least 8 characters')), + password: v.pipe(v.string('Password is required'), v.nonEmpty('Password is required'), v.minLength(15, 'Password must be at least 15 characters'), v.maxLength(128, 'Password must be 128 characters or fewer')), password_confirmation: v.pipe(v.string('Please confirm your password'), v.nonEmpty('Please confirm your password')), }), v.forward( @@ -37,7 +37,7 @@ export const forgotPasswordSchema = v.object({ export const resetPasswordSchema = v.pipe( v.object({ email: v.pipe(v.string('Email is required'), v.nonEmpty('Email is required'), v.email('Please enter a valid email')), - password: v.pipe(v.string('Password is required'), v.nonEmpty('Password is required'), v.minLength(8, 'Password must be at least 8 characters')), + password: v.pipe(v.string('Password is required'), v.nonEmpty('Password is required'), v.minLength(15, 'Password must be at least 15 characters'), v.maxLength(128, 'Password must be 128 characters or fewer')), password_confirmation: v.pipe(v.string('Please confirm your password'), v.nonEmpty('Please confirm your password')), }), v.forward( diff --git a/routes/auth.php b/routes/auth.php index 6882d23..b949b58 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -9,34 +9,38 @@ use App\Http\Controllers\Auth\ResetPasswordController; use App\Http\Controllers\Auth\SocialiteController; use App\Http\Controllers\Auth\TwoFactorChallengeController; use App\Http\Controllers\Auth\TwoFactorSettingsController; +use App\Http\Controllers\Auth\TwoFactorSetupController; use App\Http\Controllers\ProfileController; use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { Route::get('login', [LoginController::class, 'create'])->name('login'); - Route::post('login', [LoginController::class, 'store'])->middleware('throttle:5,1'); + Route::post('login', [LoginController::class, 'store'])->middleware('throttle:auth.login'); Route::get('register', [RegisterController::class, 'create'])->name('register'); - Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:3,60'); + Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:auth.register'); Route::get('forgot-password', [ForgotPasswordController::class, 'create'])->name('password.request'); - Route::post('forgot-password', [ForgotPasswordController::class, 'store'])->name('password.email')->middleware('throttle:3,15'); + Route::post('forgot-password', [ForgotPasswordController::class, 'store'])->name('password.email')->middleware('throttle:auth.password-email'); Route::get('reset-password/{token}', [ResetPasswordController::class, 'create'])->name('password.reset'); - Route::post('reset-password', [ResetPasswordController::class, 'store'])->name('password.store')->middleware('throttle:5,15'); + Route::post('reset-password', [ResetPasswordController::class, 'store'])->name('password.store')->middleware('throttle:auth.password-reset'); // Socialite routes - Route::get('auth/{provider}', [SocialiteController::class, 'redirect'])->name('socialite.redirect'); - Route::get('auth/{provider}/callback', [SocialiteController::class, 'callback'])->name('socialite.callback')->middleware('throttle:10,1'); + Route::get('auth/{provider}', [SocialiteController::class, 'redirect']) + ->middleware('throttle:auth.social') + ->name('socialite.redirect'); + Route::get('auth/{provider}/callback', [SocialiteController::class, 'callback'])->name('socialite.callback')->middleware('throttle:auth.social'); // Complete profile after social login (when username is taken) Route::get('complete-profile', [CompleteProfileController::class, 'create'])->name('auth.complete-profile'); - Route::post('complete-profile', [CompleteProfileController::class, 'store']); + Route::post('complete-profile', [CompleteProfileController::class, 'store']) + ->middleware('throttle:auth.social'); Route::get('two-factor-challenge', [TwoFactorChallengeController::class, 'create']) ->name('two-factor.login'); Route::post('two-factor-challenge', [TwoFactorChallengeController::class, 'store']) - ->middleware('throttle:5,1') + ->middleware('throttle:auth.two-factor') ->name('two-factor.login.store'); }); @@ -46,20 +50,32 @@ Route::middleware('auth')->group(function () { // Email verification routes Route::get('email/verify', [EmailVerificationController::class, 'notice'])->name('verification.notice'); Route::get('email/verify/{id}/{hash}', [EmailVerificationController::class, 'verify'])->middleware('signed')->name('verification.verify'); - Route::post('email/verification-notification', [EmailVerificationController::class, 'resend'])->middleware('throttle:6,1')->name('verification.send'); + Route::post('email/verification-notification', [EmailVerificationController::class, 'resend']) + ->middleware('throttle:auth.account') + ->name('verification.send'); Route::get('profile', ProfileController::class)->name('profile.show'); Route::redirect('security', '/profile')->name('profile.security-redirect'); + Route::get('two-factor-setup', TwoFactorSetupController::class) + ->name('two-factor.setup'); + Route::get('two-factor-setup/complete', [TwoFactorSetupController::class, 'complete']) + ->name('two-factor.setup.complete'); + Route::post('two-factor-setup', [TwoFactorSettingsController::class, 'enable']) + ->middleware('throttle:auth.two-factor') + ->name('two-factor.setup.enable'); + Route::post('two-factor-setup/confirm', [TwoFactorSettingsController::class, 'confirm']) + ->middleware('throttle:auth.two-factor') + ->name('two-factor.setup.confirm'); Route::post('profile/two-factor', [TwoFactorSettingsController::class, 'enable']) - ->middleware('throttle:5,1') + ->middleware('throttle:auth.two-factor') ->name('profile.two-factor.enable'); Route::post('profile/two-factor/confirm', [TwoFactorSettingsController::class, 'confirm']) - ->middleware('throttle:5,1') + ->middleware('throttle:auth.two-factor') ->name('profile.two-factor.confirm'); Route::delete('profile/two-factor', [TwoFactorSettingsController::class, 'disable']) - ->middleware('throttle:5,1') + ->middleware('throttle:auth.two-factor') ->name('profile.two-factor.disable'); Route::post('profile/two-factor/recovery-codes', [TwoFactorSettingsController::class, 'recoveryCodes']) - ->middleware('throttle:5,1') + ->middleware('throttle:auth.two-factor') ->name('profile.two-factor.recovery-codes'); }); diff --git a/tests/Feature/Auth/AuthSecurityHardeningTest.php b/tests/Feature/Auth/AuthSecurityHardeningTest.php new file mode 100644 index 0000000..7c51442 --- /dev/null +++ b/tests/Feature/Auth/AuthSecurityHardeningTest.php @@ -0,0 +1,226 @@ + false, + 'auth-ui.features.two_factor_required' => false, + ]); +}); + +it('returns the same password-reset response for existing and unknown accounts', function () { + Notification::fake(); + $user = User::factory()->create(['email' => 'known@example.com']); + + $expectedMessage = 'If an account exists for that email address, a password reset link has been sent.'; + + $this->post('/forgot-password', ['email' => 'known@example.com']) + ->assertSessionHas('status', $expectedMessage) + ->assertSessionDoesntHaveErrors(); + + $this->withServerVariables(['REMOTE_ADDR' => '192.0.2.2']) + ->post('/forgot-password', ['email' => 'unknown@example.com']) + ->assertSessionHas('status', $expectedMessage) + ->assertSessionDoesntHaveErrors(); + + Notification::assertSentTo($user, QueuedResetPasswordNotification::class); +}); + +it('invalidates existing sessions and notifies the user after password reset', function () { + Notification::fake(); + config(['session.driver' => 'database']); + $user = User::factory()->create(['email' => 'reset@example.com']); + + DB::table('sessions')->insert([ + [ + 'id' => 'existing-session-one', + 'user_id' => $user->id, + 'ip_address' => '192.0.2.10', + 'user_agent' => 'Test', + 'payload' => 'payload', + 'last_activity' => now()->timestamp, + ], + [ + 'id' => 'existing-session-two', + 'user_id' => $user->id, + 'ip_address' => '192.0.2.11', + 'user_agent' => 'Test', + 'payload' => 'payload', + 'last_activity' => now()->timestamp, + ], + ]); + + $token = Password::createToken($user); + + $this->post('/reset-password', [ + 'token' => $token, + 'email' => $user->email, + 'password' => 'a completely new secure password', + 'password_confirmation' => 'a completely new secure password', + ])->assertRedirect('/login'); + + $user->refresh(); + + expect(DB::table('sessions')->where('user_id', $user->id)->count())->toBe(0) + ->and(Hash::check('a completely new secure password', $user->password))->toBeTrue() + ->and($user->auth_session_version)->toBe(1); + Notification::assertSentTo($user, PasswordChangedNotification::class); +}); + +it('stores new passwords with Argon2id', function () { + $this->post('/register', [ + 'username' => 'argon-user', + 'first_name' => 'Argon', + 'last_name' => 'User', + 'email' => 'argon@example.com', + 'password' => 'correct horse battery staple', + 'password_confirmation' => 'correct horse battery staple', + ])->assertRedirect('/dashboard'); + + $user = User::where('email', 'argon@example.com')->firstOrFail(); + + expect(password_get_info($user->password)['algoName'])->toBe('argon2id'); +}); + +it('rejects new passwords shorter than fifteen characters', function () { + $this->post('/register', [ + 'username' => 'short-password', + 'first_name' => 'Short', + 'last_name' => 'Password', + 'email' => 'short@example.com', + 'password' => 'too-short', + 'password_confirmation' => 'too-short', + ])->assertSessionHasErrors('password'); + + $this->assertDatabaseMissing('users', ['email' => 'short@example.com']); +}); + +it('rejects password hashes created with an unexpected algorithm', function () { + $user = User::factory()->create(['email' => 'unexpected-hash@example.com']); + DB::table('users')->where('id', $user->id)->update([ + 'password' => password_hash('correct-password', PASSWORD_BCRYPT, ['cost' => 4]), + ]); + + $this->post('/login', [ + 'login' => 'unexpected-hash@example.com', + 'password' => 'correct-password', + ])->assertSessionHasErrors('login'); + + $this->assertGuest(); + expect(password_get_info($user->fresh()->password)['algoName'])->toBe('bcrypt'); +}); + +it('enforces account-aware login throttling across different IP addresses', function () { + User::factory()->create([ + 'email' => 'limited@example.com', + 'password' => 'correct-password', + ]); + + foreach (range(1, 10) as $attempt) { + $this->withServerVariables(['REMOTE_ADDR' => "192.0.2.{$attempt}"]) + ->post('/login', [ + 'login' => 'limited@example.com', + 'password' => 'wrong-password', + ]) + ->assertSessionHasErrors('login'); + } + + $this->withServerVariables(['REMOTE_ADDR' => '192.0.2.11']) + ->post('/login', [ + 'login' => 'limited@example.com', + 'password' => 'wrong-password', + ]) + ->assertTooManyRequests(); +}); + +it('adds browser security headers and prevents caching secret-bearing pages', function () { + $this->get('/') + ->assertHeader('X-Content-Type-Options', 'nosniff') + ->assertHeader('X-Frame-Options', 'DENY') + ->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin') + ->assertHeader('Permissions-Policy'); + + $response = $this->get('/login') + ->assertHeader('Cache-Control') + ->assertHeader('Pragma', 'no-cache'); + + expect($response->headers->get('Cache-Control'))->toContain('no-store'); +}); + +it('adds CSP and HSTS to secure production responses', function () { + $previousEnvironment = $this->app->environment(); + $this->app->instance('env', 'production'); + + try { + $this->withServerVariables([ + 'HTTPS' => 'on', + 'REQUEST_SCHEME' => 'https', + 'SERVER_PORT' => 443, + ]) + ->get('https://localhost/') + ->assertHeader('Content-Security-Policy') + ->assertHeader( + 'Strict-Transport-Security', + 'max-age=63072000; includeSubDomains; preload' + ); + } finally { + $this->app->instance('env', $previousEnvironment); + } +}); + +it('ends an authenticated session at its absolute expiry', function () { + $user = User::factory()->create(); + + $this->actingAs($user) + ->withSession(['auth.absolute_expires_at' => now()->subSecond()->timestamp]) + ->get('/dashboard') + ->assertRedirect('/login') + ->assertSessionHas('error', 'Your session has expired. Please sign in again.'); + + $this->assertGuest(); +}); + +it('records the session version and absolute expiry after login', function () { + $user = User::factory()->create([ + 'email' => 'session-version@example.com', + 'password' => 'correct horse battery staple', + 'auth_session_version' => 3, + ]); + + $response = $this->post('/login', [ + 'login' => $user->email, + 'password' => 'correct horse battery staple', + ])->assertRedirect('/dashboard') + ->assertSessionHas('auth.session_version', 3); + + expect($response->getSession()->get('auth.absolute_expires_at')) + ->toBeGreaterThan(now()->timestamp); +}); + +it('revokes an authenticated session when its user session version is stale', function () { + $user = User::factory()->create(['auth_session_version' => 2]); + + $this->actingAs($user) + ->withSession([ + 'auth.session_version' => 1, + 'auth.absolute_expires_at' => now()->addHour()->timestamp, + ]) + ->get('/dashboard') + ->assertRedirect('/login') + ->assertSessionHas( + 'error', + 'Your session is no longer valid. Please sign in again.' + ); + + $this->assertGuest(); +}); diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php index 7165c4c..a54e2d6 100644 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -8,6 +8,10 @@ use Illuminate\Support\Facades\URL; uses(RefreshDatabase::class); +beforeEach(function () { + config(['auth-ui.features.two_factor_required' => false]); +}); + it('does not send verification email when feature is disabled', function () { config(['auth-ui.features.email_verification' => false]); @@ -18,8 +22,8 @@ it('does not send verification email when feature is disabled', function () { 'first_name' => 'Test', 'last_name' => 'User', 'email' => 'test@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', + 'password' => 'correct horse battery staple', + 'password_confirmation' => 'correct horse battery staple', ]); Notification::assertNothingSent(); @@ -35,8 +39,8 @@ it('sends verification email when feature is enabled', function () { 'first_name' => 'Test', 'last_name' => 'User', 'email' => 'test@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', + 'password' => 'correct horse battery staple', + 'password_confirmation' => 'correct horse battery staple', ]); $user = User::where('email', 'test@example.com')->first(); @@ -135,8 +139,8 @@ it('redirects to verification notice after registration when feature is enabled' 'first_name' => 'New', 'last_name' => 'User', 'email' => 'new@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', + 'password' => 'correct horse battery staple', + 'password_confirmation' => 'correct horse battery staple', ])->assertRedirect('/email/verify'); }); @@ -148,7 +152,7 @@ it('redirects to home after registration when feature is disabled', function () 'first_name' => 'New', 'last_name' => 'User', 'email' => 'new@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', + 'password' => 'correct horse battery staple', + 'password_confirmation' => 'correct horse battery staple', ])->assertRedirect('/dashboard'); }); diff --git a/tests/Feature/Auth/LoginTest.php b/tests/Feature/Auth/LoginTest.php index e254968..2d9093c 100644 --- a/tests/Feature/Auth/LoginTest.php +++ b/tests/Feature/Auth/LoginTest.php @@ -5,6 +5,10 @@ use Illuminate\Foundation\Testing\RefreshDatabase; uses(RefreshDatabase::class); +beforeEach(function () { + config(['auth-ui.features.two_factor_required' => false]); +}); + it('allows login with email and password', function () { $user = User::factory()->create([ 'email' => 'test@example.com', diff --git a/tests/Feature/Auth/SocialLoginSecurityTest.php b/tests/Feature/Auth/SocialLoginSecurityTest.php index eb14fce..f3f9ac0 100644 --- a/tests/Feature/Auth/SocialLoginSecurityTest.php +++ b/tests/Feature/Auth/SocialLoginSecurityTest.php @@ -9,6 +9,7 @@ use Laravel\Socialite\Two\User as SocialiteUser; uses(RefreshDatabase::class); beforeEach(function () { + config(['auth-ui.features.two_factor_required' => false]); config(['auth-ui.providers.github' => [ 'label' => 'GitHub', 'icon' => 'i-simple-icons-github', diff --git a/tests/Feature/Auth/TwoFactorAuthenticationTest.php b/tests/Feature/Auth/TwoFactorAuthenticationTest.php index 26a1dbe..db584f4 100644 --- a/tests/Feature/Auth/TwoFactorAuthenticationTest.php +++ b/tests/Feature/Auth/TwoFactorAuthenticationTest.php @@ -1,8 +1,11 @@ true, 'auth-ui.features.two_factor_required' => false, 'fortify.features' => [Features::twoFactorAuthentication()], - 'fortify-options.two-factor-authentication' => ['confirm' => true], + 'fortify-options.two-factor-authentication' => [ + 'confirm' => true, + 'secret-length' => 32, + 'window' => 0, + ], 'auth-ui.providers.github' => [ 'label' => 'GitHub', 'icon' => 'i-simple-icons-github', @@ -81,12 +88,19 @@ it('redirects authenticated users to enrollment when two-factor setup is mandato $this->actingAs($user) ->get('/dashboard') - ->assertRedirect('/profile') + ->assertRedirect('/two-factor-setup') ->assertSessionHas('error', 'Two-factor authentication is required before you can continue.'); + $this->actingAs($user) + ->get('/two-factor-setup') + ->assertInertia(fn (Assert $page) => $page + ->component('Auth/TwoFactorSetup') + ->where('twoFactor.enabled', false) + ->where('twoFactor.pending', false)); + $this->actingAs($user) ->get('/profile') - ->assertOk(); + ->assertRedirect('/two-factor-setup'); $this->actingAs($user) ->post('/logout') @@ -94,20 +108,63 @@ it('redirects authenticated users to enrollment when two-factor setup is mandato $this->assertGuest(); }); +it('sends a password user directly to setup after login when enrollment is mandatory', function () { + config(['auth-ui.features.two_factor_required' => true]); + $user = User::factory()->create([ + 'email' => 'setup@example.com', + 'password' => 'password123', + ]); + + $this->post('/login', [ + 'login' => 'setup@example.com', + 'password' => 'password123', + ])->assertRedirect('/two-factor-setup'); + + $this->assertAuthenticatedAs($user); + expect(session('url.intended'))->toEndWith('/dashboard'); +}); + it('keeps application access restricted while mandatory enrollment is pending', function () { config(['auth-ui.features.two_factor_required' => true]); $user = User::factory()->create(['password' => 'password123']); $this->actingAs($user) - ->post('/profile/two-factor', ['password' => 'password123']) - ->assertRedirect('/profile'); + ->post('/two-factor-setup', ['password' => 'password123']) + ->assertRedirect('/two-factor-setup'); expect($user->fresh()->two_factor_secret)->not->toBeNull() ->and($user->fresh()->hasEnabledTwoFactorAuthentication())->toBeFalse(); $this->actingAs($user) ->get('/dashboard') - ->assertRedirect('/profile'); + ->assertRedirect('/two-factor-setup'); +}); + +it('shows recovery codes before continuing after mandatory setup', function () { + config(['auth-ui.features.two_factor_required' => true]); + $user = User::factory()->create(); + app(EnableTwoFactorAuthentication::class)($user); + $user->refresh(); + + $secret = Fortify::currentEncrypter()->decrypt($user->two_factor_secret); + $code = app(Google2FA::class)->getCurrentOtp($secret); + + $this->actingAs($user) + ->withSession(['url.intended' => '/dashboard']) + ->post('/two-factor-setup/confirm', ['code' => $code]) + ->assertRedirect('/two-factor-setup') + ->assertSessionHas('recoveryCodes', fn (array $codes) => count($codes) === 8); + + $this->actingAs($user->fresh()) + ->get('/two-factor-setup') + ->assertInertia(fn (Assert $page) => $page + ->component('Auth/TwoFactorSetup') + ->where('twoFactor.enabled', true)); + + $this->actingAs($user->fresh()) + ->withSession(['url.intended' => '/dashboard']) + ->get('/two-factor-setup/complete') + ->assertRedirect('/dashboard'); }); it('allows normal access after mandatory enrollment is complete', function () { @@ -120,6 +177,21 @@ it('allows normal access after mandatory enrollment is complete', function () { ->assertOk(); }); +it('does not allow mandatory two-factor authentication to be disabled', function () { + config(['auth-ui.features.two_factor_required' => true]); + $user = User::factory()->create(['password' => 'password123']); + $twoFactor = enableTwoFactorForTest($user); + + $this->actingAs($user) + ->delete('/profile/two-factor', [ + 'password' => 'password123', + 'code' => $twoFactor['code'], + ]) + ->assertForbidden(); + + expect($user->fresh()->hasEnabledTwoFactorAuthentication())->toBeTrue(); +}); + it('does not enforce the mandatory flag while the main two-factor feature is disabled', function () { config([ 'auth-ui.features.two_factor' => false, @@ -131,6 +203,20 @@ it('does not enforce the mandatory flag while the main two-factor feature is dis ->assertOk(); }); +it('does not expose mandatory setup endpoints when enrollment is optional', function () { + $user = User::factory()->create(['password' => 'password123']); + + $this->actingAs($user) + ->get('/two-factor-setup') + ->assertNotFound(); + + $this->actingAs($user) + ->post('/two-factor-setup', ['password' => 'password123']) + ->assertNotFound(); + + expect($user->fresh()->two_factor_secret)->toBeNull(); +}); + it('blocks authenticated API requests until mandatory enrollment is complete', function () { config(['auth-ui.features.two_factor_required' => true]); $user = User::factory()->create(); @@ -140,7 +226,7 @@ it('blocks authenticated API requests until mandatory enrollment is complete', f ->assertForbidden() ->assertJson([ 'message' => 'Two-factor authentication setup is required.', - 'setup_url' => route('profile.show'), + 'setup_url' => route('two-factor.setup'), ]); enableTwoFactorForTest($user); @@ -195,6 +281,7 @@ it('confirms enrollment and returns recovery codes once', function () { }); it('holds password login until a valid authenticator code is supplied', function () { + Notification::fake(); $user = User::factory()->create([ 'email' => 'test@example.com', 'password' => 'password123', @@ -216,6 +303,7 @@ it('holds password login until a valid authenticator code is supplied', function $this->post('/two-factor-challenge', ['code' => '000000']) ->assertSessionHasErrors('code'); $this->assertGuest(); + Notification::assertSentTo($user, TwoFactorFailedNotification::class); $challengeResponse = $this->post('/two-factor-challenge', ['code' => $twoFactor['code']]); $challengeResponse->assertRedirect('/dashboard'); @@ -283,6 +371,94 @@ it('requires a valid second factor for social-only users to disable protection', expect($user->fresh()->hasEnabledTwoFactorAuthentication())->toBeFalse(); }); +it('requires both password and second factor for password users to disable protection', function () { + Notification::fake(); + $user = User::factory()->create(['password' => 'password123']); + $twoFactor = enableTwoFactorForTest($user); + + $this->actingAs($user) + ->delete('/profile/two-factor', ['password' => 'password123']) + ->assertSessionHasErrors('code'); + + expect($user->fresh()->hasEnabledTwoFactorAuthentication())->toBeTrue(); + + $this->actingAs($user) + ->delete('/profile/two-factor', [ + 'password' => 'password123', + 'code' => $twoFactor['code'], + ]) + ->assertRedirect('/profile'); + + expect($user->fresh()->hasEnabledTwoFactorAuthentication())->toBeFalse(); + Notification::assertSentTo($user, TwoFactorSecurityNotification::class); +}); + +it('requires both password and second factor to reveal recovery codes', function () { + $user = User::factory()->create(['password' => 'password123']); + $twoFactor = enableTwoFactorForTest($user); + + $this->actingAs($user) + ->post('/profile/two-factor/recovery-codes', [ + 'password' => 'password123', + 'regenerate' => false, + ]) + ->assertSessionHasErrors('code'); + + $this->actingAs($user) + ->post('/profile/two-factor/recovery-codes', [ + 'password' => 'password123', + 'code' => $twoFactor['code'], + 'regenerate' => false, + ]) + ->assertRedirect('/profile') + ->assertSessionHas('recoveryCodes'); +}); + +it('expires an unfinished second-factor login after five minutes', function () { + $user = User::factory()->create([ + 'email' => 'expires@example.com', + 'password' => 'password123', + ]); + enableTwoFactorForTest($user); + + $this->post('/login', [ + 'login' => 'expires@example.com', + 'password' => 'password123', + ])->assertRedirect('/two-factor-challenge'); + + $this->travel(301)->seconds(); + + $this->get('/two-factor-challenge') + ->assertRedirect('/login') + ->assertSessionMissing('login.id') + ->assertSessionMissing('login.started_at'); +}); + +it('does not accept the same TOTP twice', function () { + $user = User::factory()->create([ + 'email' => 'replay@example.com', + 'password' => 'password123', + ]); + $twoFactor = enableTwoFactorForTest($user); + + $this->post('/login', [ + 'login' => 'replay@example.com', + 'password' => 'password123', + ])->assertRedirect('/two-factor-challenge'); + $this->post('/two-factor-challenge', ['code' => $twoFactor['code']]) + ->assertRedirect('/dashboard'); + + $this->post('/logout'); + $this->post('/login', [ + 'login' => 'replay@example.com', + 'password' => 'password123', + ])->assertRedirect('/two-factor-challenge'); + $this->post('/two-factor-challenge', ['code' => $twoFactor['code']]) + ->assertSessionHasErrors('code'); + + $this->assertGuest(); +}); + it('consumes recovery codes used to authorize a sensitive settings action', function () { $user = User::factory()->social()->create(); $twoFactor = enableTwoFactorForTest($user); diff --git a/tests/Pest.php b/tests/Pest.php index 60f04a4..3834758 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,7 @@ extend(Tests\TestCase::class) +pest()->extend(TestCase::class) // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); diff --git a/vitest.config.ts b/vitest.config.ts index 52f9f2d..c63f4c4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ }, resolve: { alias: { - '@': resolve(__dirname, './resources/js'), + '@': resolve(import.meta.dirname, './resources/js'), }, }, })