From ac340d125c4123dbbfb9204a61ed10eb23819c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20van=20de=20Wouw?= Date: Sun, 12 Jul 2026 14:44:58 +0200 Subject: [PATCH] Initial commit Certificate manager built on Laravel 13, Livewire 4, and Flux. Co-Authored-By: Claude Sonnet 5 --- .claude/settings.local.json | 7 + .editorconfig | 18 + .env.example | 65 + .gitattributes | 11 + .github/dependabot.yml | 12 + .github/workflows/lint.yml | 51 + .github/workflows/tests.yml | 67 + .gitignore | 24 + .npmrc | 2 + README.md | 44 + app/Actions/Fortify/CreateNewUser.php | 33 + app/Actions/Fortify/ResetUserPassword.php | 29 + app/Concerns/PasswordValidationRules.php | 29 + app/Concerns/ProfileValidationRules.php | 51 + .../Commands/SendCertificateReminders.php | 97 + app/Enums/ExpiryStatus.php | 28 + app/Enums/Locale.php | 25 + .../CertificateDownloadController.php | 24 + app/Http/Controllers/Controller.php | 8 + app/Http/Controllers/LanguageController.php | 29 + app/Http/Controllers/MarketingController.php | 23 + .../SharedCertificateController.php | 25 + app/Http/Middleware/SetLocale.php | 31 + app/Livewire/Actions/Logout.php | 24 + app/Mail/SharedCertificateLinksMail.php | 42 + app/Mail/SharedCertificatesMail.php | 50 + app/Models/Category.php | 36 + app/Models/Certificate.php | 64 + app/Models/Concerns/OwnedByUser.php | 27 + app/Models/Scopes/OwnedByUserScope.php | 26 + app/Models/User.php | 94 + .../CertificateExpiredNotification.php | 35 + .../CertificateExpiringNotification.php | 39 + app/Ocr/OcrManager.php | 46 + app/Ocr/OcrProviderInterface.php | 15 + app/Ocr/Providers/KlippaProvider.php | 97 + app/Ocr/Providers/MistralProvider.php | 117 + app/Ocr/Providers/NoneProvider.php | 18 + app/Providers/AppServiceProvider.php | 50 + app/Providers/FortifyServiceProvider.php | 80 + app/Services/CertificateZipper.php | 74 + artisan | 18 + boost.json | 15 + bootstrap/app.php | 22 + bootstrap/cache/.gitignore | 2 + bootstrap/providers.php | 9 + composer.json | 117 + composer.lock | 11342 ++++++++++++++++ config/app.php | 126 + config/auth.php | 117 + config/cache.php | 136 + config/database.php | 184 + config/filesystems.php | 80 + config/fortify.php | 177 + config/logging.php | 132 + config/mail.php | 118 + config/queue.php | 129 + config/services.php | 38 + config/session.php | 233 + database/.gitignore | 1 + database/factories/CategoryFactory.php | 24 + database/factories/CertificateFactory.php | 45 + database/factories/UserFactory.php | 60 + .../0001_01_01_000000_create_users_table.php | 49 + .../0001_01_01_000001_create_cache_table.php | 35 + .../0001_01_01_000002_create_jobs_table.php | 59 + ...024_01_01_000000_create_passkeys_table.php | 34 + ..._add_two_factor_columns_to_users_table.php | 34 + ...6_07_08_000001_create_categories_table.php | 25 + ...07_08_000002_create_certificates_table.php | 28 + ...dd_certificate_settings_to_users_table.php | 24 + ...last_reminded_at_to_certificates_table.php | 22 + ...0001_add_details_to_certificates_table.php | 25 + ...07_10_000001_add_locale_to_users_table.php | 22 + database/seeders/DatabaseSeeder.php | 25 + lang/nl.json | 224 + package-lock.json | 1600 +++ package.json | 24 + phpstan.neon | 13 + phpunit.xml | 36 + pint.json | 3 + public/.htaccess | 25 + public/apple-touch-icon.png | Bin 0 -> 1662 bytes public/favicon.ico | Bin 0 -> 4286 bytes public/favicon.svg | 3 + public/index.php | 20 + public/robots.txt | 2 + resources/css/app.css | 128 + resources/js/app.js | 0 resources/js/marketing.js | 42 + resources/js/passkeys.js | 4 + .../views/components/app-logo-icon.blade.php | 8 + resources/views/components/app-logo.blade.php | 17 + .../views/components/auth-header.blade.php | 9 + .../components/auth-session-status.blade.php | 9 + .../components/desktop-user-menu.blade.php | 39 + .../components/language-switcher.blade.php | 24 + .../components/passkey-registration.blade.php | 116 + .../views/components/passkey-verify.blade.php | 76 + .../components/placeholder-pattern.blade.php | 12 + resources/views/dashboard.blade.php | 18 + .../views/flux/icon/book-open-text.blade.php | 47 + .../flux/icon/chevrons-up-down.blade.php | 43 + .../views/flux/icon/folder-git-2.blade.php | 45 + .../views/flux/icon/layout-grid.blade.php | 45 + resources/views/flux/navlist/group.blade.php | 51 + resources/views/layouts/app.blade.php | 5 + resources/views/layouts/app/header.blade.php | 84 + resources/views/layouts/app/sidebar.blade.php | 120 + resources/views/layouts/auth.blade.php | 3 + resources/views/layouts/auth/card.blade.php | 33 + resources/views/layouts/auth/simple.blade.php | 33 + resources/views/layouts/auth/split.blade.php | 50 + .../mail/shared-certificate-links.blade.php | 18 + .../views/mail/shared-certificates.blade.php | 12 + resources/views/marketing/home.blade.php | 518 + resources/views/marketing/layout.blade.php | 38 + .../views/marketing/partials/cta.blade.php | 26 + .../views/marketing/partials/faq.blade.php | 36 + .../views/marketing/partials/footer.blade.php | 51 + .../views/marketing/partials/nav.blade.php | 118 + .../marketing/partials/pricing-grid.blade.php | 96 + resources/views/marketing/pricing.blade.php | 70 + resources/views/marketing/security.blade.php | 101 + .../pages/auth/confirm-password.blade.php | 36 + .../pages/auth/forgot-password.blade.php | 31 + resources/views/pages/auth/login.blade.php | 59 + resources/views/pages/auth/register.blade.php | 69 + .../views/pages/auth/reset-password.blade.php | 54 + .../pages/auth/two-factor-challenge.blade.php | 101 + .../views/pages/auth/verify-email.blade.php | 29 + .../views/pages/categories/⚡index.blade.php | 119 + .../pages/certificates/⚡create.blade.php | 197 + .../views/pages/certificates/⚡edit.blade.php | 142 + .../pages/certificates/⚡index.blade.php | 352 + .../views/pages/settings/layout.blade.php | 21 + .../two-factor/⚡recovery-codes.blade.php | 136 + .../pages/settings/⚡appearance.blade.php | 47 + .../pages/settings/⚡certificates.blade.php | 91 + .../settings/⚡delete-user-form.blade.php | 20 + .../settings/⚡delete-user-modal.blade.php | 50 + .../views/pages/settings/⚡profile.blade.php | 124 + .../views/pages/settings/⚡security.blade.php | 341 + .../⚡two-factor-setup-modal.blade.php | 310 + resources/views/pages/⚡dashboard.blade.php | 199 + resources/views/partials/head.blade.php | 15 + .../views/partials/settings-heading.blade.php | 5 + resources/views/welcome.blade.php | 202 + routes/console.php | 11 + routes/settings.php | 28 + routes/web.php | 32 + storage/app/.gitignore | 4 + storage/app/private/.gitignore | 2 + storage/app/public/.gitignore | 2 + storage/framework/.gitignore | 9 + storage/framework/cache/.gitignore | 3 + storage/framework/cache/data/.gitignore | 2 + storage/framework/sessions/.gitignore | 2 + storage/framework/testing/.gitignore | 2 + storage/framework/views/.gitignore | 2 + storage/logs/.gitignore | 2 + tests/Feature/Auth/AuthenticationTest.php | 67 + tests/Feature/Auth/EmailVerificationTest.php | 72 + .../Feature/Auth/PasswordConfirmationTest.php | 11 + tests/Feature/Auth/PasswordResetTest.php | 65 + tests/Feature/Auth/RegistrationTest.php | 27 + tests/Feature/Auth/TwoFactorChallengeTest.php | 28 + .../Categories/CategoryManagementTest.php | 87 + .../Certificates/CertificateActionsTest.php | 73 + .../Certificates/CertificateIndexTest.php | 117 + .../Certificates/CertificateReminderTest.php | 51 + .../Certificates/CertificateSharingTest.php | 105 + .../Certificates/CertificateUploadTest.php | 123 + tests/Feature/DashboardTest.php | 57 + tests/Feature/ExampleTest.php | 7 + tests/Feature/LocaleTest.php | 50 + tests/Feature/Models/OwnershipTest.php | 81 + tests/Feature/Ocr/OcrProviderTest.php | 82 + .../Settings/CertificateSettingsTest.php | 55 + tests/Feature/Settings/ProfileUpdateTest.php | 75 + tests/Feature/Settings/SecurityTest.php | 113 + tests/Pest.php | 50 + tests/TestCase.php | 16 + tests/Unit/ExampleTest.php | 5 + vite.config.js | 32 + 185 files changed, 23569 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 app/Actions/Fortify/CreateNewUser.php create mode 100644 app/Actions/Fortify/ResetUserPassword.php create mode 100644 app/Concerns/PasswordValidationRules.php create mode 100644 app/Concerns/ProfileValidationRules.php create mode 100644 app/Console/Commands/SendCertificateReminders.php create mode 100644 app/Enums/ExpiryStatus.php create mode 100644 app/Enums/Locale.php create mode 100644 app/Http/Controllers/CertificateDownloadController.php create mode 100644 app/Http/Controllers/Controller.php create mode 100644 app/Http/Controllers/LanguageController.php create mode 100644 app/Http/Controllers/MarketingController.php create mode 100644 app/Http/Controllers/SharedCertificateController.php create mode 100644 app/Http/Middleware/SetLocale.php create mode 100644 app/Livewire/Actions/Logout.php create mode 100644 app/Mail/SharedCertificateLinksMail.php create mode 100644 app/Mail/SharedCertificatesMail.php create mode 100644 app/Models/Category.php create mode 100644 app/Models/Certificate.php create mode 100644 app/Models/Concerns/OwnedByUser.php create mode 100644 app/Models/Scopes/OwnedByUserScope.php create mode 100644 app/Models/User.php create mode 100644 app/Notifications/CertificateExpiredNotification.php create mode 100644 app/Notifications/CertificateExpiringNotification.php create mode 100644 app/Ocr/OcrManager.php create mode 100644 app/Ocr/OcrProviderInterface.php create mode 100644 app/Ocr/Providers/KlippaProvider.php create mode 100644 app/Ocr/Providers/MistralProvider.php create mode 100644 app/Ocr/Providers/NoneProvider.php create mode 100644 app/Providers/AppServiceProvider.php create mode 100644 app/Providers/FortifyServiceProvider.php create mode 100644 app/Services/CertificateZipper.php create mode 100755 artisan create mode 100644 boost.json create mode 100644 bootstrap/app.php create mode 100644 bootstrap/cache/.gitignore create mode 100644 bootstrap/providers.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/cache.php create mode 100644 config/database.php create mode 100644 config/filesystems.php create mode 100644 config/fortify.php create mode 100644 config/logging.php create mode 100644 config/mail.php create mode 100644 config/queue.php create mode 100644 config/services.php create mode 100644 config/session.php create mode 100644 database/.gitignore create mode 100644 database/factories/CategoryFactory.php create mode 100644 database/factories/CertificateFactory.php create mode 100644 database/factories/UserFactory.php create mode 100644 database/migrations/0001_01_01_000000_create_users_table.php create mode 100644 database/migrations/0001_01_01_000001_create_cache_table.php create mode 100644 database/migrations/0001_01_01_000002_create_jobs_table.php create mode 100644 database/migrations/2024_01_01_000000_create_passkeys_table.php create mode 100644 database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php create mode 100644 database/migrations/2026_07_08_000001_create_categories_table.php create mode 100644 database/migrations/2026_07_08_000002_create_certificates_table.php create mode 100644 database/migrations/2026_07_08_000003_add_certificate_settings_to_users_table.php create mode 100644 database/migrations/2026_07_08_000004_add_last_reminded_at_to_certificates_table.php create mode 100644 database/migrations/2026_07_09_000001_add_details_to_certificates_table.php create mode 100644 database/migrations/2026_07_10_000001_add_locale_to_users_table.php create mode 100644 database/seeders/DatabaseSeeder.php create mode 100644 lang/nl.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100644 pint.json create mode 100644 public/.htaccess create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon.ico create mode 100644 public/favicon.svg create mode 100644 public/index.php create mode 100644 public/robots.txt create mode 100644 resources/css/app.css create mode 100644 resources/js/app.js create mode 100644 resources/js/marketing.js create mode 100644 resources/js/passkeys.js create mode 100644 resources/views/components/app-logo-icon.blade.php create mode 100644 resources/views/components/app-logo.blade.php create mode 100644 resources/views/components/auth-header.blade.php create mode 100644 resources/views/components/auth-session-status.blade.php create mode 100644 resources/views/components/desktop-user-menu.blade.php create mode 100644 resources/views/components/language-switcher.blade.php create mode 100644 resources/views/components/passkey-registration.blade.php create mode 100644 resources/views/components/passkey-verify.blade.php create mode 100644 resources/views/components/placeholder-pattern.blade.php create mode 100644 resources/views/dashboard.blade.php create mode 100644 resources/views/flux/icon/book-open-text.blade.php create mode 100644 resources/views/flux/icon/chevrons-up-down.blade.php create mode 100644 resources/views/flux/icon/folder-git-2.blade.php create mode 100644 resources/views/flux/icon/layout-grid.blade.php create mode 100644 resources/views/flux/navlist/group.blade.php create mode 100644 resources/views/layouts/app.blade.php create mode 100644 resources/views/layouts/app/header.blade.php create mode 100644 resources/views/layouts/app/sidebar.blade.php create mode 100644 resources/views/layouts/auth.blade.php create mode 100644 resources/views/layouts/auth/card.blade.php create mode 100644 resources/views/layouts/auth/simple.blade.php create mode 100644 resources/views/layouts/auth/split.blade.php create mode 100644 resources/views/mail/shared-certificate-links.blade.php create mode 100644 resources/views/mail/shared-certificates.blade.php create mode 100644 resources/views/marketing/home.blade.php create mode 100644 resources/views/marketing/layout.blade.php create mode 100644 resources/views/marketing/partials/cta.blade.php create mode 100644 resources/views/marketing/partials/faq.blade.php create mode 100644 resources/views/marketing/partials/footer.blade.php create mode 100644 resources/views/marketing/partials/nav.blade.php create mode 100644 resources/views/marketing/partials/pricing-grid.blade.php create mode 100644 resources/views/marketing/pricing.blade.php create mode 100644 resources/views/marketing/security.blade.php create mode 100644 resources/views/pages/auth/confirm-password.blade.php create mode 100644 resources/views/pages/auth/forgot-password.blade.php create mode 100644 resources/views/pages/auth/login.blade.php create mode 100644 resources/views/pages/auth/register.blade.php create mode 100644 resources/views/pages/auth/reset-password.blade.php create mode 100644 resources/views/pages/auth/two-factor-challenge.blade.php create mode 100644 resources/views/pages/auth/verify-email.blade.php create mode 100644 resources/views/pages/categories/⚡index.blade.php create mode 100644 resources/views/pages/certificates/⚡create.blade.php create mode 100644 resources/views/pages/certificates/⚡edit.blade.php create mode 100644 resources/views/pages/certificates/⚡index.blade.php create mode 100644 resources/views/pages/settings/layout.blade.php create mode 100644 resources/views/pages/settings/two-factor/⚡recovery-codes.blade.php create mode 100644 resources/views/pages/settings/⚡appearance.blade.php create mode 100644 resources/views/pages/settings/⚡certificates.blade.php create mode 100644 resources/views/pages/settings/⚡delete-user-form.blade.php create mode 100644 resources/views/pages/settings/⚡delete-user-modal.blade.php create mode 100644 resources/views/pages/settings/⚡profile.blade.php create mode 100644 resources/views/pages/settings/⚡security.blade.php create mode 100644 resources/views/pages/settings/⚡two-factor-setup-modal.blade.php create mode 100644 resources/views/pages/⚡dashboard.blade.php create mode 100644 resources/views/partials/head.blade.php create mode 100644 resources/views/partials/settings-heading.blade.php create mode 100644 resources/views/welcome.blade.php create mode 100644 routes/console.php create mode 100644 routes/settings.php create mode 100644 routes/web.php create mode 100644 storage/app/.gitignore create mode 100644 storage/app/private/.gitignore create mode 100644 storage/app/public/.gitignore create mode 100644 storage/framework/.gitignore create mode 100644 storage/framework/cache/.gitignore create mode 100644 storage/framework/cache/data/.gitignore create mode 100644 storage/framework/sessions/.gitignore create mode 100644 storage/framework/testing/.gitignore create mode 100644 storage/framework/views/.gitignore create mode 100644 storage/logs/.gitignore create mode 100644 tests/Feature/Auth/AuthenticationTest.php create mode 100644 tests/Feature/Auth/EmailVerificationTest.php create mode 100644 tests/Feature/Auth/PasswordConfirmationTest.php create mode 100644 tests/Feature/Auth/PasswordResetTest.php create mode 100644 tests/Feature/Auth/RegistrationTest.php create mode 100644 tests/Feature/Auth/TwoFactorChallengeTest.php create mode 100644 tests/Feature/Categories/CategoryManagementTest.php create mode 100644 tests/Feature/Certificates/CertificateActionsTest.php create mode 100644 tests/Feature/Certificates/CertificateIndexTest.php create mode 100644 tests/Feature/Certificates/CertificateReminderTest.php create mode 100644 tests/Feature/Certificates/CertificateSharingTest.php create mode 100644 tests/Feature/Certificates/CertificateUploadTest.php create mode 100644 tests/Feature/DashboardTest.php create mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Feature/LocaleTest.php create mode 100644 tests/Feature/Models/OwnershipTest.php create mode 100644 tests/Feature/Ocr/OcrProviderTest.php create mode 100644 tests/Feature/Settings/CertificateSettingsTest.php create mode 100644 tests/Feature/Settings/ProfileUpdateTest.php create mode 100644 tests/Feature/Settings/SecurityTest.php create mode 100644 tests/Pest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ExampleTest.php create mode 100644 vite.config.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..96ecba9 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(vendor/bin/phpstan analyse *)" + ] + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6df8428 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[{compose,docker-compose}.{yml,yaml}] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c0660ea --- /dev/null +++ b/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4da48a8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +CHANGELOG.md export-ignore +README.md export-ignore +.github/workflows/browser-tests.yml export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ae67c4d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 5 + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a3183e1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,51 @@ +name: linter + +on: + push: + branches: + - develop + - main + - master + - workos + pull_request: + branches: + - develop + - main + - master + - workos + +permissions: + contents: write + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: '8.4' + + - name: Add Flux Credentials Loaded From ENV + run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_USERNAME }}" "${{ secrets.FLUX_LICENSE_KEY }}" + + - name: Install Dependencies + run: | + composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + npm install + + - name: Run Pint + run: composer lint + + # - name: Commit Changes + # uses: stefanzweifel/git-auto-commit-action@v7 + # with: + # commit_message: fix code style + # commit_options: '--no-verify' + # file_pattern: | + # **/* + # !.github/workflows/* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e35f56f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,67 @@ +name: tests + +on: + push: + branches: + - develop + - main + - master + - workos + pull_request: + branches: + - develop + - main + - master + - workos + +permissions: + contents: read + +jobs: + ci: + runs-on: ubuntu-latest + strategy: + matrix: + php-version: ['8.3', '8.4', '8.5'] + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer:v2 + coverage: xdebug + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: '22' + + - name: Install Node Dependencies + run: npm i + + - name: Add Flux Credentials Loaded From ENV + run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_USERNAME }}" "${{ secrets.FLUX_LICENSE_KEY }}" + + - name: Install Dependencies + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: Copy Environment File + run: cp .env.example .env + + - name: Generate Application Key + run: php artisan key:generate + + - name: Build Assets + run: npm run build + + - name: Run Type Analysis + run: composer types:check + + - name: Run Tests + run: php artisan test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b43fd4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +/.phpunit.cache +/node_modules +/public/build +/public/fonts-manifest.dev.json +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +/auth.json +/.fleet +/.idea +/.nova +/.vscode +/.zed diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..495a6af --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +ignore-scripts=true +audit=true diff --git a/README.md b/README.md new file mode 100644 index 0000000..de54b2b --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Certified + +Certified is a self-hosted certificate manager built on Laravel 13, Livewire 4, and Flux. It helps you keep track of certificates (diplomas, licenses, compliance documents, etc.), organize them into categories, and get notified before they expire. + +## Features + +- **Certificate management** — upload, edit, and download certificates, organized by category +- **OCR extraction** — automatically read certificate details on upload via a pluggable OCR backend (Klippa, Mistral, or none) +- **Expiry notifications** — email alerts when a certificate is expiring soon or has expired +- **Secure sharing** — share a certificate with an external recipient via a signed URL, without requiring them to log in +- **Multi-language support** + +## Stack + +- Laravel 13 (Octane-ready) +- Livewire 4 + Flux UI +- Tailwind CSS 4 / Vite +- Laravel Fortify for authentication + +## Getting started + +```bash +composer install +npm install + +cp .env.example .env +php artisan key:generate + +php artisan migrate + +npm run dev +# in another terminal +php artisan serve +``` + +## Testing + +```bash +composer test +``` + +## OCR providers + +The OCR backend is configurable via `app/Ocr`. Available providers: `NoneProvider` (disabled), `KlippaProvider`, and `MistralProvider`. Configure credentials in `.env`. diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..3c7c00c --- /dev/null +++ b/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,33 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + ...$this->profileRules(), + 'password' => $this->passwordRules(), + ])->validate(); + + return User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => $input['password'], + ]); + } +} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..8fda5dd --- /dev/null +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,29 @@ + $input + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => $input['password'], + ])->save(); + } +} diff --git a/app/Concerns/PasswordValidationRules.php b/app/Concerns/PasswordValidationRules.php new file mode 100644 index 0000000..7409ed8 --- /dev/null +++ b/app/Concerns/PasswordValidationRules.php @@ -0,0 +1,29 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } + + /** + * Get the validation rules used to validate the current password. + * + * @return array|string> + */ + protected function currentPasswordRules(): array + { + return ['required', 'string', 'current_password']; + } +} diff --git a/app/Concerns/ProfileValidationRules.php b/app/Concerns/ProfileValidationRules.php new file mode 100644 index 0000000..a9c069b --- /dev/null +++ b/app/Concerns/ProfileValidationRules.php @@ -0,0 +1,51 @@ +|string>> + */ + protected function profileRules(?int $userId = null): array + { + return [ + 'name' => $this->nameRules(), + 'email' => $this->emailRules($userId), + ]; + } + + /** + * Get the validation rules used to validate user names. + * + * @return array|string> + */ + protected function nameRules(): array + { + return ['required', 'string', 'max:255']; + } + + /** + * Get the validation rules used to validate user emails. + * + * @return array|string> + */ + protected function emailRules(?int $userId = null): array + { + return [ + 'required', + 'string', + 'email', + 'max:255', + $userId === null + ? Rule::unique(User::class) + : Rule::unique(User::class)->ignore($userId), + ]; + } +} diff --git a/app/Console/Commands/SendCertificateReminders.php b/app/Console/Commands/SendCertificateReminders.php new file mode 100644 index 0000000..21803fd --- /dev/null +++ b/app/Console/Commands/SendCertificateReminders.php @@ -0,0 +1,97 @@ +sendAdvanceReminders($today); + $urgentCount = $this->sendExpiryReminders($today); + + $this->info("Sent {$advanceCount} advance reminder(s) and {$urgentCount} expiry reminder(s)."); + + return self::SUCCESS; + } + + /** + * Part 1: for each user, notify about certificates expiring exactly on + * their personal "months before" threshold. + */ + private function sendAdvanceReminders(Carbon $today): int + { + $sent = 0; + + User::query() + ->whereHas('certificates') + ->with('certificates') + ->chunkById(100, function ($users) use ($today, &$sent) { + foreach ($users as $user) { + $targetDate = $today->copy()->addMonths($user->reminder_months_before); + + $certificates = $user->certificates() + ->whereDate('expires_at', $targetDate) + ->tap($this->notRemindedToday($today)) + ->get(); + + foreach ($certificates as $certificate) { + $user->notify(new CertificateExpiringNotification($certificate)); + $certificate->forceFill(['last_reminded_at' => now()])->save(); + $sent++; + } + } + }); + + return $sent; + } + + /** + * Part 2: notify about certificates expiring today (across all users). + */ + private function sendExpiryReminders(Carbon $today): int + { + $sent = 0; + + Certificate::query() + ->whereDate('expires_at', $today) + ->tap($this->notRemindedToday($today)) + ->with('user') + ->chunkById(100, function ($certificates) use (&$sent) { + foreach ($certificates as $certificate) { + $certificate->user?->notify(new CertificateExpiredNotification($certificate)); + $certificate->forceFill(['last_reminded_at' => now()])->save(); + $sent++; + } + }); + + return $sent; + } + + /** + * Skip certificates already reminded today, so a re-run (or a missed day + * followed by a catch-up run) never double-sends. + */ + private function notRemindedToday(Carbon $today): callable + { + return function (Builder $query) use ($today) { + $query->where(function (Builder $query) use ($today) { + $query->whereNull('last_reminded_at') + ->orWhereDate('last_reminded_at', '<', $today); + }); + }; + } +} diff --git a/app/Enums/ExpiryStatus.php b/app/Enums/ExpiryStatus.php new file mode 100644 index 0000000..614f8ff --- /dev/null +++ b/app/Enums/ExpiryStatus.php @@ -0,0 +1,28 @@ + 'red', + self::ExpiringSoon => 'amber', + self::Valid => 'green', + }; + } + + public function label(): string + { + return match ($this) { + self::Expired => __('Expired'), + self::ExpiringSoon => __('Expiring soon'), + self::Valid => __('Valid'), + }; + } +} diff --git a/app/Enums/Locale.php b/app/Enums/Locale.php new file mode 100644 index 0000000..daccb8c --- /dev/null +++ b/app/Enums/Locale.php @@ -0,0 +1,25 @@ + __('English'), + self::Dutch => __('Dutch'), + }; + } + + /** + * @return array + */ + public static function values(): array + { + return array_map(fn (self $locale) => $locale->value, self::cases()); + } +} diff --git a/app/Http/Controllers/CertificateDownloadController.php b/app/Http/Controllers/CertificateDownloadController.php new file mode 100644 index 0000000..c963c87 --- /dev/null +++ b/app/Http/Controllers/CertificateDownloadController.php @@ -0,0 +1,24 @@ +file_path) && Storage::disk('local')->exists($certificate->file_path), 404); + + return Storage::disk('local')->download( + $certificate->file_path, + $certificate->title.'.'.pathinfo($certificate->file_path, PATHINFO_EXTENSION), + ); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ + $locale]); + + if ($request->user()) { + $request->user()->locale = $locale; + $request->user()->save(); + } + + return back(); + } +} diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php new file mode 100644 index 0000000..5c8cea1 --- /dev/null +++ b/app/Http/Controllers/MarketingController.php @@ -0,0 +1,23 @@ +file_path) && Storage::disk('local')->exists($certificate->file_path), 404); + + return Storage::disk('local')->download( + $certificate->file_path, + $certificate->title.'.'.pathinfo($certificate->file_path, PATHINFO_EXTENSION), + ); + } +} diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php new file mode 100644 index 0000000..624b623 --- /dev/null +++ b/app/Http/Middleware/SetLocale.php @@ -0,0 +1,31 @@ +locale + : session('locale') ?? $request->getPreferredLanguage(Locale::values()); + + if (in_array($locale, Locale::values(), true)) { + App::setLocale($locale); + } + + return $next($request); + } +} diff --git a/app/Livewire/Actions/Logout.php b/app/Livewire/Actions/Logout.php new file mode 100644 index 0000000..457de43 --- /dev/null +++ b/app/Livewire/Actions/Logout.php @@ -0,0 +1,24 @@ +logout(); + + Session::invalidate(); + Session::regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Mail/SharedCertificateLinksMail.php b/app/Mail/SharedCertificateLinksMail.php new file mode 100644 index 0000000..2ed627b --- /dev/null +++ b/app/Mail/SharedCertificateLinksMail.php @@ -0,0 +1,42 @@ + $links + */ + public function __construct( + public string $senderName, + public array $links, + public string $expiresAt, + ) {} + + public function envelope(): Envelope + { + return new Envelope( + subject: __(':name shared certificates with you', ['name' => $this->senderName]), + ); + } + + public function content(): Content + { + return new Content( + markdown: 'mail.shared-certificate-links', + with: [ + 'senderName' => $this->senderName, + 'links' => $this->links, + 'expiresAt' => $this->expiresAt, + ], + ); + } +} diff --git a/app/Mail/SharedCertificatesMail.php b/app/Mail/SharedCertificatesMail.php new file mode 100644 index 0000000..87c6a69 --- /dev/null +++ b/app/Mail/SharedCertificatesMail.php @@ -0,0 +1,50 @@ + $this->senderName]), + ); + } + + public function content(): Content + { + return new Content( + markdown: 'mail.shared-certificates', + with: ['senderName' => $this->senderName], + ); + } + + /** + * @return array + */ + public function attachments(): array + { + return [ + Attachment::fromPath($this->zipPath) + ->as('certificates.zip') + ->withMime('application/zip'), + ]; + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..3495682 --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,36 @@ + */ + use HasFactory, OwnedByUser; + + /** + * @return HasMany + */ + public function certificates(): HasMany + { + return $this->hasMany(Certificate::class); + } +} diff --git a/app/Models/Certificate.php b/app/Models/Certificate.php new file mode 100644 index 0000000..3d24453 --- /dev/null +++ b/app/Models/Certificate.php @@ -0,0 +1,64 @@ + */ + use HasFactory, OwnedByUser; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'issued_at' => 'date', + 'expires_at' => 'date', + ]; + } + + /** + * @return BelongsTo + */ + public function category(): BelongsTo + { + return $this->belongsTo(Category::class); + } + + public function expiryStatus(int $monthsBefore = 1): ExpiryStatus + { + return match (true) { + $this->expires_at->isPast() && ! $this->expires_at->isToday() => ExpiryStatus::Expired, + $this->expires_at->lte(today()->addMonths($monthsBefore)) => ExpiryStatus::ExpiringSoon, + default => ExpiryStatus::Valid, + }; + } +} diff --git a/app/Models/Concerns/OwnedByUser.php b/app/Models/Concerns/OwnedByUser.php new file mode 100644 index 0000000..6d7ab64 --- /dev/null +++ b/app/Models/Concerns/OwnedByUser.php @@ -0,0 +1,27 @@ +user_id) && ($user = Auth::user()) !== null) { + $model->user_id = $user->id; + } + }); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Scopes/OwnedByUserScope.php b/app/Models/Scopes/OwnedByUserScope.php new file mode 100644 index 0000000..e38d5ed --- /dev/null +++ b/app/Models/Scopes/OwnedByUserScope.php @@ -0,0 +1,26 @@ + + */ +class OwnedByUserScope implements Scope +{ + /** + * Restrict queries to the authenticated user's rows. Deliberately a + * no-op without an authenticated user so console commands and queued + * jobs can operate across all users. + */ + public function apply(Builder $builder, Model $model): void + { + if (Auth::hasUser()) { + $builder->where($model->qualifyColumn('user_id'), Auth::id()); + } + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..2a76c52 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,94 @@ + */ + use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable; + + /** + * @var array + */ + protected $attributes = [ + 'reminder_months_before' => 1, + 'ocr_provider' => 'none', + 'locale' => 'en', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'reminder_months_before' => 'integer', + 'ocr_api_key' => 'encrypted', + ]; + } + + /** + * @return HasMany + */ + public function certificates(): HasMany + { + return $this->hasMany(Certificate::class); + } + + /** + * @return HasMany + */ + public function categories(): HasMany + { + return $this->hasMany(Category::class); + } + + /** + * Get the user's initials + */ + public function initials(): string + { + $initials = Str::initials($this->name, true); + + return Str::length($initials) > 1 + ? Str::substr($initials, 0, 1).Str::substr($initials, -1) + : $initials; + } +} diff --git a/app/Notifications/CertificateExpiredNotification.php b/app/Notifications/CertificateExpiredNotification.php new file mode 100644 index 0000000..4d0e2a3 --- /dev/null +++ b/app/Notifications/CertificateExpiredNotification.php @@ -0,0 +1,35 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(User $notifiable): MailMessage + { + return (new MailMessage) + ->error() + ->subject(__('Certificate expires today: :title', ['title' => $this->certificate->title])) + ->greeting(__('Hello :name,', ['name' => $notifiable->name])) + ->line(__('Your certificate ":title" expires today.', ['title' => $this->certificate->title])) + ->action(__('View your certificates'), route('certificates.index')) + ->line(__('Renew it as soon as possible to avoid a lapse in compliance.')); + } +} diff --git a/app/Notifications/CertificateExpiringNotification.php b/app/Notifications/CertificateExpiringNotification.php new file mode 100644 index 0000000..714d69c --- /dev/null +++ b/app/Notifications/CertificateExpiringNotification.php @@ -0,0 +1,39 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(User $notifiable): MailMessage + { + $date = $this->certificate->expires_at->toFormattedDateString(); + + return (new MailMessage) + ->subject(__('Certificate expiring soon: :title', ['title' => $this->certificate->title])) + ->greeting(__('Hello :name,', ['name' => $notifiable->name])) + ->line(__('Your certificate ":title" will expire on :date.', [ + 'title' => $this->certificate->title, + 'date' => $date, + ])) + ->action(__('View your certificates'), route('certificates.index')) + ->line(__('Please renew it in good time to stay compliant.')); + } +} diff --git a/app/Ocr/OcrManager.php b/app/Ocr/OcrManager.php new file mode 100644 index 0000000..0d08077 --- /dev/null +++ b/app/Ocr/OcrManager.php @@ -0,0 +1,46 @@ + + */ + public static function availableProviders(): array + { + return ['none', 'klippa', 'mistral']; + } +} diff --git a/app/Ocr/OcrProviderInterface.php b/app/Ocr/OcrProviderInterface.php new file mode 100644 index 0000000..a22698b --- /dev/null +++ b/app/Ocr/OcrProviderInterface.php @@ -0,0 +1,15 @@ + $filePath]); + + return []; + } + + try { + $response = Http::withHeaders(['X-Auth-Key' => $apiKey]) + ->timeout(30) + ->attach('document', $contents, basename($filePath)) + ->post(self::ENDPOINT, ['template' => 'financial_full']); + + if ($response->failed()) { + Log::warning('Klippa OCR request failed.', ['status' => $response->status()]); + + return []; + } + + $parsed = $response->json('data.parsed', []); + + return array_filter([ + 'title' => $this->extractTitle($parsed), + 'expires_at' => $this->extractDate($parsed), + ], fn ($value) => $value !== null); + } catch (Throwable $e) { + Log::warning('Klippa OCR analysis threw an exception.', ['message' => $e->getMessage()]); + + return []; + } + } + + /** + * @param array $parsed + */ + private function extractTitle(array $parsed): ?string + { + foreach (['document_subject', 'merchant_name', 'title'] as $key) { + $value = data_get($parsed, $key); + + if (filled($value) && is_string($value)) { + return trim($value); + } + } + + return null; + } + + /** + * @param array $parsed + */ + private function extractDate(array $parsed): ?string + { + foreach (['expiry_date', 'valid_until', 'date'] as $key) { + $value = data_get($parsed, $key); + + if (filled($value) && is_string($value)) { + try { + return CarbonImmutable::parse($value)->toDateString(); + } catch (Throwable) { + continue; + } + } + } + + return null; + } +} diff --git a/app/Ocr/Providers/MistralProvider.php b/app/Ocr/Providers/MistralProvider.php new file mode 100644 index 0000000..1fab645 --- /dev/null +++ b/app/Ocr/Providers/MistralProvider.php @@ -0,0 +1,117 @@ +toDataUri($filePath); + + if ($dataUri === null) { + return []; + } + + try { + $response = Http::withToken($apiKey) + ->timeout(45) + ->post(self::ENDPOINT, [ + 'model' => self::MODEL, + 'response_format' => ['type' => 'json_object'], + 'messages' => [[ + 'role' => 'user', + 'content' => [ + ['type' => 'text', 'text' => self::PROMPT], + ['type' => 'image_url', 'image_url' => $dataUri], + ], + ]], + ]); + + if ($response->failed()) { + Log::warning('Mistral OCR request failed.', ['status' => $response->status()]); + + return []; + } + + $content = $response->json('choices.0.message.content'); + + return $this->parseContent(is_string($content) ? $content : ''); + } catch (Throwable $e) { + Log::warning('Mistral OCR analysis threw an exception.', ['message' => $e->getMessage()]); + + return []; + } + } + + private function toDataUri(string $filePath): ?string + { + $mime = mime_content_type($filePath) ?: 'image/jpeg'; + + // Pixtral is a vision model and only accepts images, not PDFs. + if (! str_starts_with($mime, 'image/')) { + return null; + } + + return 'data:'.$mime.';base64,'.base64_encode((string) file_get_contents($filePath)); + } + + /** + * @return array{title?: string|null, expires_at?: string|null} + */ + private function parseContent(string $content): array + { + $decoded = json_decode($content, true); + + if (! is_array($decoded)) { + return []; + } + + $title = data_get($decoded, 'title'); + $expiresAt = data_get($decoded, 'expires_at'); + + return array_filter([ + 'title' => filled($title) && is_string($title) ? trim($title) : null, + 'expires_at' => $this->normalizeDate($expiresAt), + ], fn ($value) => $value !== null); + } + + private function normalizeDate(mixed $value): ?string + { + if (! filled($value) || ! is_string($value)) { + return null; + } + + try { + return CarbonImmutable::parse($value)->toDateString(); + } catch (Throwable) { + return null; + } + } +} diff --git a/app/Ocr/Providers/NoneProvider.php b/app/Ocr/Providers/NoneProvider.php new file mode 100644 index 0000000..73b7d7c --- /dev/null +++ b/app/Ocr/Providers/NoneProvider.php @@ -0,0 +1,18 @@ +configureDefaults(); + } + + /** + * Configure default behaviors for production-ready applications. + */ + protected function configureDefaults(): void + { + Date::use(CarbonImmutable::class); + + DB::prohibitDestructiveCommands( + app()->isProduction(), + ); + + Password::defaults(fn (): ?Password => app()->isProduction() + ? Password::min(12) + ->mixedCase() + ->letters() + ->numbers() + ->symbols() + ->uncompromised() + : null, + ); + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php new file mode 100644 index 0000000..753f862 --- /dev/null +++ b/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,80 @@ +configureActions(); + $this->configureViews(); + $this->configureRateLimiting(); + } + + /** + * Configure Fortify actions. + */ + private function configureActions(): void + { + Fortify::resetUserPasswordsUsing(ResetUserPassword::class); + Fortify::createUsersUsing(CreateNewUser::class); + } + + /** + * Configure Fortify views. + */ + private function configureViews(): void + { + Fortify::loginView(fn () => view('pages::auth.login')); + Fortify::verifyEmailView(fn () => view('pages::auth.verify-email')); + Fortify::twoFactorChallengeView(fn () => view('pages::auth.two-factor-challenge')); + Fortify::confirmPasswordView(fn () => view('pages::auth.confirm-password')); + Fortify::registerView(fn () => view('pages::auth.register')); + Fortify::resetPasswordView(fn () => view('pages::auth.reset-password')); + Fortify::requestPasswordResetLinkView(fn () => view('pages::auth.forgot-password')); + } + + /** + * Configure rate limiting. + */ + private function configureRateLimiting(): void + { + RateLimiter::for('two-factor', function (Request $request) { + return Limit::perMinute(5)->by($request->session()->get('login.id')); + }); + + RateLimiter::for('login', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + + RateLimiter::for('passkeys', function (Request $request) { + $credentialId = $request->input('credential.id'); + + return Limit::perMinute(10)->by( + ($credentialId ?: $request->session()->getId()).'|'.$request->ip(), + ); + }); + } +} diff --git a/app/Services/CertificateZipper.php b/app/Services/CertificateZipper.php new file mode 100644 index 0000000..f35ab8c --- /dev/null +++ b/app/Services/CertificateZipper.php @@ -0,0 +1,74 @@ + $certificates + */ + public function create(Collection $certificates, string $password): string + { + $archivePath = tempnam(sys_get_temp_dir(), 'certs_').'.zip'; + + $zip = new ZipArchive; + + if ($zip->open($archivePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + throw new RuntimeException('Unable to create ZIP archive.'); + } + + $zip->setPassword($password); + $usedNames = []; + + foreach ($certificates as $certificate) { + if (blank($certificate->file_path) || ! Storage::disk('local')->exists($certificate->file_path)) { + continue; + } + + $entryName = $this->uniqueEntryName($certificate, $usedNames); + $usedNames[] = $entryName; + + $zip->addFromString($entryName, Storage::disk('local')->get($certificate->file_path)); + $zip->setEncryptionName($entryName, ZipArchive::EM_AES_256); + } + + if ($zip->count() === 0) { + $zip->close(); + @unlink($archivePath); + + throw new RuntimeException('No files were available to add to the archive.'); + } + + $zip->close(); + + return $archivePath; + } + + /** + * @param array $usedNames + */ + private function uniqueEntryName(Certificate $certificate, array $usedNames): string + { + $extension = pathinfo($certificate->file_path, PATHINFO_EXTENSION); + $base = Str::slug($certificate->title) ?: 'certificate'; + $name = $base.'.'.$extension; + $counter = 1; + + while (in_array($name, $usedNames, true)) { + $name = $base.'-'.(++$counter).'.'.$extension; + } + + return $name; + } +} diff --git a/artisan b/artisan new file mode 100755 index 0000000..c35e31d --- /dev/null +++ b/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/boost.json b/boost.json new file mode 100644 index 0000000..55ee4cc --- /dev/null +++ b/boost.json @@ -0,0 +1,15 @@ +{ + "cloud": false, + "guidelines": true, + "mcp": true, + "nightwatch": false, + "sail": false, + "skills": [ + "fortify-development", + "laravel-best-practices", + "fluxui-development", + "livewire-development", + "pest-testing", + "tailwindcss-development" + ] +} diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..fb6abcf --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,22 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->web(append: [SetLocale::class]); + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->shouldRenderJsonWhen( + fn (Request $request) => $request->is('api/*') || $request->expectsJson(), + ); + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..5ffd769 --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,9 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.13.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "bcd989ad36c92d42a3715379af91f2defee5b8dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/bcd989ad36c92d42a3715379af91f2defee5b8dd", + "reference": "bcd989ad36c92d42a3715379af91f2defee5b8dd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12.3", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.6", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.13.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-07-05T19:00:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-06-02T12:23:43+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.12.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.12.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-06-23T15:21:08+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.8", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/9c19128923b05a5d7355e5d2318d7808b7e33bbd", + "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2026-06-23T13:02:23+00:00" + }, + { + "name": "laravel/chisel", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/chisel.git", + "reference": "c2ac11cd1c6b763b412aa26b9d95fce70a489d31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/chisel/zipball/c2ac11cd1c6b763b412aa26b9d95fce70a489d31", + "reference": "c2ac11cd1c6b763b412aa26b9d95fce70a489d31", + "shasum": "" + }, + "require": { + "illuminate/process": "^10.20|^11.0|^12.0|^13.0", + "nikic/php-parser": "^5.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.24.0", + "pestphp/pest": "^4.1.0", + "phpstan/phpstan": "^2.1.26", + "rector/rector": "^2.1.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laravel\\Chisel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Toolkit for building scripts that remove unwanted code, files, and dependencies.", + "keywords": [ + "chisel", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/chisel/issues", + "source": "https://github.com/laravel/chisel/tree/v0.1.1" + }, + "time": "2026-05-13T21:29:24+00:00" + }, + { + "name": "laravel/fortify", + "version": "v1.37.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/fortify.git", + "reference": "5d4b6a53527edd19ecc4f13e8e74ec91bdefab0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/fortify/zipball/5d4b6a53527edd19ecc4f13e8e74ec91bdefab0c", + "reference": "5d4b6a53527edd19ecc4f13e8e74ec91bdefab0c", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "laravel/passkeys": "^0.2.0", + "php": "^8.2", + "pragmarx/google2fa": "^9.0" + }, + "require-dev": { + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Fortify\\FortifyServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Fortify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Backend controllers and scaffolding for Laravel authentication.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/fortify/issues", + "source": "https://github.com/laravel/fortify" + }, + "time": "2026-05-15T22:59:10+00:00" + }, + { + "name": "laravel/framework", + "version": "v13.19.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "514502b38e11bd676ecf83b271c9452cc7500f16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/514502b38e11bd676ecf83b271c9452cc7500f16", + "reference": "514502b38e11bd676ecf83b271c9452cc7500f16", + "shasum": "" + }, + "require": { + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1 || 2.0", + "psr/log-implementation": "1.0 || 2.0 || 3.0", + "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/psr7": "^2.9", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^11.0.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "^2.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-07-07T14:13:33+00:00" + }, + { + "name": "laravel/passkeys", + "version": "v0.2.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/passkeys-server.git", + "reference": "a76656ada41b2b4a591f075eddae5ddc67e8ab9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passkeys-server/zipball/a76656ada41b2b4a591f075eddae5ddc67e8ab9c", + "reference": "a76656ada41b2b4a591f075eddae5ddc67e8ab9c", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/http": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "web-auth/webauthn-lib": "5.3.x" + }, + "require-dev": { + "laravel/pint": "^1.28.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "rector/rector": "^2.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Passkeys\\PasskeysServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passkeys\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Passwordless authentication using WebAuthn/passkeys for Laravel", + "homepage": "https://github.com/laravel/passkeys-server", + "keywords": [ + "Authentication", + "Passwordless", + "laravel", + "passkeys", + "webauthn" + ], + "support": { + "issues": "https://github.com/laravel/passkeys-server/issues", + "source": "https://github.com/laravel/passkeys-server" + }, + "time": "2026-05-18T16:26:00+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.21", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "7753c65c281c2550c7c183f14e18062073b7d821" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/7753c65c281c2550c7c183f14e18062073b7d821", + "reference": "7753c65c281c2550c7c183f14e18062073b7d821", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.21" + }, + "time": "2026-06-26T00:11:25+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-16T14:03:50+00:00" + }, + { + "name": "laravel/tinker", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "4faba77764bd33411735936acdf30446d058c78b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.2" + }, + "time": "2026-03-17T14:54:13+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.35.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "b277b5dc3d56650b68904117124e79c851e12376" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/b277b5dc3d56650b68904117124e79c851e12376", + "reference": "b277b5dc3d56650b68904117124e79c851e12376", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.35.2" + }, + "time": "2026-07-06T14:42:07+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "livewire/blaze", + "version": "v1.0.12", + "source": { + "type": "git", + "url": "https://github.com/livewire/blaze.git", + "reference": "6f7d21aac44e542261590c744aa959129c9a04ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/blaze/zipball/6f7d21aac44e542261590c744aa959129c9a04ed", + "reference": "6f7d21aac44e542261590c744aa959129c9a04ed", + "shasum": "" + }, + "require": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/view": "^10.0|^11.0|^12.0|^13.0", + "nikic/php-parser": "^5.0", + "php": "^8.1" + }, + "conflict": { + "livewire/flux": "<2.12.1" + }, + "require-dev": { + "livewire/flux": "dev-main", + "orchestra/testbench": "^8.0|^9.0|^10.0|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^2.0|^3.0|^4.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Livewire\\Blaze\\BlazeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Livewire\\Blaze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio" + } + ], + "description": "A tool for optimizing Blade component performance by folding them into parent templates", + "support": { + "issues": "https://github.com/livewire/blaze/issues", + "source": "https://github.com/livewire/blaze/tree/v1.0.12" + }, + "time": "2026-04-06T09:24:26+00:00" + }, + { + "name": "livewire/flux", + "version": "v2.15.0", + "source": { + "type": "git", + "url": "https://github.com/livewire/flux.git", + "reference": "c570fb836278e9be881d08b7e386a1a62ab9faf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/flux/zipball/c570fb836278e9be881d08b7e386a1a62ab9faf7", + "reference": "c570fb836278e9be881d08b7e386a1a62ab9faf7", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/view": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1|^0.2|^0.3", + "livewire/livewire": "^3.7.4|^4.0", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "conflict": { + "livewire/blaze": "<1.0.0-beta.2" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Flux": "Flux\\Flux" + }, + "providers": [ + "Flux\\FluxServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Flux\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "proprietary" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "The official UI component library for Livewire.", + "keywords": [ + "components", + "flux", + "laravel", + "livewire", + "ui" + ], + "support": { + "issues": "https://github.com/livewire/flux/issues", + "source": "https://github.com/livewire/flux/tree/v2.15.0" + }, + "time": "2026-06-19T02:25:18+00:00" + }, + { + "name": "livewire/livewire", + "version": "v4.3.3", + "source": { + "type": "git", + "url": "https://github.com/livewire/livewire.git", + "reference": "8021f2561865c4c297a3bfca37212a99034377e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/livewire/zipball/8021f2561865c4c297a3bfca37212a99034377e7", + "reference": "8021f2561865c4c297a3bfca37212a99034377e7", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/routing": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", + "league/mime-type-detection": "^1.9", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-kernel": "^6.2|^7.0|^8.0" + }, + "require-dev": { + "calebporzio/sushi": "^2.1", + "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.3.1", + "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0", + "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.5", + "psy/psysh": "^0.11.22|^0.12" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Livewire": "Livewire\\Livewire" + }, + "providers": [ + "Livewire\\LivewireServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "A front-end framework for Laravel.", + "support": { + "issues": "https://github.com/livewire/livewire/issues", + "source": "https://github.com/livewire/livewire/tree/v4.3.3" + }, + "funding": [ + { + "url": "https://github.com/livewire", + "type": "github" + } + ], + "time": "2026-06-27T03:16:11+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.13.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-06-18T13:49:15+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.4" + }, + "time": "2026-05-11T20:49:54+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + }, + "time": "2026-07-08T07:01:06+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v9.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" + }, + "time": "2025-09-19T22:51:08+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.24", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" + }, + "time": "2026-06-29T15:41:09+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8", + "shasum": "" + }, + "require": { + "brick/math": ">=0.8.16 <=0.18", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.3" + }, + "time": "2026-06-18T03:57:49+00:00" + }, + { + "name": "spomky-labs/cbor-php", + "version": "3.2.3", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", + "reference": "dd6eb84e6d92f7b8bd0da56b4b4dd7235aed0c32", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "ext-json": "*", + "roave/security-advisories": "dev-latest", + "symfony/error-handler": "^6.4|^7.1|^8.0", + "symfony/var-dumper": "^6.4|^7.1|^8.0" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", + "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "CBOR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + } + ], + "description": "CBOR Encoder/Decoder for PHP", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "cbor" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.2.3" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-04-01T12:15:20+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/aa576cbd07128075bef97ac2f8af9854e67513d8", + "reference": "aa576cbd07128075bef97ac2f8af9854e67513d8", + "shasum": "" + }, + "require": { + "brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "ext-mbstring": "*", + "php": ">=8.1", + "psr/clock": "^1.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", + "ext-gmp": "*", + "ext-openssl": "*", + "infection/infection": "^0.28|^0.29|^0.31|^0.32", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3|^2.0", + "phpstan/phpstan": "^1.8|^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.1|^2.0", + "phpstan/phpstan-strict-rules": "^1.3|^2.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0|^13.0", + "rector/rector": "^1.0|^2.0", + "roave/security-advisories": "dev-latest", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symplify/easy-coding-standard": "^12.0|^13.0" + }, + "suggest": { + "ext-bcmath": "For better performance (or GMP)", + "ext-gmp": "For better performance (or BCMath)", + "ext-openssl": "For OpenSSL based cyphering" + }, + "type": "library", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + }, + { + "name": "Florent Morselli", + "email": "florent.morselli@spomky-labs.com", + "role": "Spomky-Labs PKI Framework developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.2" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-03-23T22:56:56+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", + "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T12:55:20+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "abd6c11dc468725d1627302ad10f6cd486e9e3d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abd6c11dc468725d1627302ad10f6cd486e9e3d0", + "reference": "abd6c11dc468725d1627302ad10f6cd486e9e3d0", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-09T12:28:30+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e2989e762c70f9490fa3a00a0ac0fae5aa97a531" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e2989e762c70f9490fa3a00a0ac0fae5aa97a531", + "reference": "e2989e762c70f9490fa3a00a0ac0fae5aa97a531", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-27T09:05:56+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "6a168c8fcee806b57ac020244da14293d1f9a883" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6a168c8fcee806b57ac020244da14293d1f9a883", + "reference": "6a168c8fcee806b57ac020244da14293d1f9a883", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" + }, + "require-dev": { + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-12T08:43:41+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "89d8d6e7fbab3d9eda89ccb5ecdf44a74c4ec9d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/89d8d6e7fbab3d9eda89ccb5ecdf44a74c4ec9d2", + "reference": "89d8d6e7fbab3d9eda89ccb5ecdf44a74c4ec9d2", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/translation-contracts": "<2.5", + "symfony/var-dumper": "<8.1", + "symfony/web-profiler-bundle": "<8.1", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^8.1", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "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" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-27T09:27:36+00:00" + }, + { + "name": "symfony/mailer", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "4fa583a7377f28d54e4de442fba76375b2e20a12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/4fa583a7377f28d54e4de442fba76375b2e20a12", + "reference": "4fa583a7377f28d54e4de442fba76375b2e20a12", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.4.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T12:55:20+00:00" + }, + { + "name": "symfony/mime", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b164ae7e3f7915aacfe9ee155f2f358502440664", + "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T05:58:03+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "dc21118016c039a66235cf93d96b435ffb282412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T15:22:23+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T02:25:22+00:00" + }, + { + "name": "symfony/polyfill-php86", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php86\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T11:52:35+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/property-access", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "9261ef060f26cc7b728f67f141ba19b98a6209a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/9261ef060f26cc7b728f67f141ba19b98a6209a9", + "reference": "9261ef060f26cc7b728f67f141ba19b98a6209a9", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/property-info": "^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/property-info", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/4721e8c56d0cd2378e0ef9a9899f810008b859f7", + "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/string": "^7.4|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/routing", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/serializer", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "f911b744bc24658f435ea30439cfe536f0173a3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/f911b744bc24658f435ea30439cfe536f0173a3a", + "reference": "f911b744bc24658f435ea30439cfe536f0173a3a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/property-access": "<8.1", + "symfony/property-info": "<7.4", + "symfony/type-info": "<7.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "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/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "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" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-27T09:05:56+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T09:55:08+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "342b4218630dc2cf284cedcb2080c80b13404014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/342b4218630dc2cf284cedcb2080c80b13404014", + "reference": "342b4218630dc2cf284cedcb2080c80b13404014", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-06T11:11:44+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/type-info", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7", + "reference": "9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/uid", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "7393f157a55f7e70a4de0334435c55a5a8fe749a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/7393f157a55f7e70a4de0334435c55a5a8fe749a", + "reference": "7393f157a55f7e70a4de0334435c55a5a8fe749a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "40096a2515a979f3125c5c928603995b8664c62a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/40096a2515a979f3125c5c928603995b8664c62a", + "reference": "40096a2515a979f3125c5c928603995b8664c62a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-09T10:54:51+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.4", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/416df702837983f8d5ff48c9c3fee4f5f57b980b", + "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2026-07-06T19:11:50+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + }, + { + "name": "web-auth/cose-lib", + "version": "4.5.2", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "reference": "5b38660f90070a8e45f3dbc9528ade3b608dd77d", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12|^0.13|^0.14|^0.15|^0.16|^0.17", + "ext-json": "*", + "ext-openssl": "*", + "php": ">=8.1", + "spomky-labs/pki-framework": "^1.0" + }, + "require-dev": { + "spomky-labs/cbor-php": "^3.2.2" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension", + "spomky-labs/cbor-php": "For COSE Signature support" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.5.2" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-05-03T09:49:50+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "5.3.5", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "9e0986d999f4102e24ac8a598d3a80d98b56c19f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/9e0986d999f4102e24ac8a598d3a80d98b56c19f", + "reference": "9e0986d999f4102e24ac8a598d3a80d98b56c19f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "paragonie/constant_time_encoding": "^2.6|^3.0", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.3|^6.0", + "psr/clock": "^1.0", + "psr/event-dispatcher": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/cbor-php": "^3.0", + "spomky-labs/pki-framework": "^1.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^3.2", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "web-auth/cose-lib": "^4.2.3" + }, + "suggest": { + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/event-dispatcher": "Recommended to use dispatched events", + "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/web-auth/webauthn-framework", + "name": "web-auth/webauthn-framework" + } + }, + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.5" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-05-31T15:00:08+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.4.1" + }, + "time": "2026-06-15T15:31:57+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.20.0", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "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" + }, + "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" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "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" + }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "iamcal/sql-parser", + "version": "v0.7", + "source": { + "type": "git", + "url": "https://github.com/iamcal/SQLParser.git", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8", + "shasum": "" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1.0", + "phpunit/phpunit": "^5|^6|^7|^8|^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "iamcal\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cal Henderson", + "email": "cal@iamcal.com" + } + ], + "description": "MySQL schema parser", + "support": { + "issues": "https://github.com/iamcal/SQLParser/issues", + "source": "https://github.com/iamcal/SQLParser/tree/v0.7" + }, + "time": "2026-01-28T22:20:33+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "larastan/larastan", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/larastan/larastan.git", + "reference": "2970f83398154178a739609c244577267c7ee8eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/larastan/larastan/zipball/2970f83398154178a739609c244577267c7ee8eb", + "reference": "2970f83398154178a739609c244577267c7ee8eb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "iamcal/sql-parser": "^0.7.0", + "illuminate/console": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/container": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/database": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/http": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/support": "^11.44.2 || ^12.4.1 || ^13", + "php": "^8.2", + "phpstan/phpstan": "^2.2.0" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "laravel/framework": "^11.44.2 || ^12.7.2 || ^13", + "mockery/mockery": "^1.6.12", + "nikic/php-parser": "^5.4", + "orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11", + "orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8 || ^13.1.8" + }, + "suggest": { + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench", + "phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Larastan\\Larastan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Can Vural", + "email": "can9119@gmail.com" + } + ], + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "larastan", + "laravel", + "package", + "php", + "static analysis" + ], + "support": { + "issues": "https://github.com/larastan/larastan/issues", + "source": "https://github.com/larastan/larastan/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://github.com/canvural", + "type": "github" + } + ], + "time": "2026-05-28T08:00:58+00:00" + }, + { + "name": "laravel/agent-detector", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/agent-detector.git", + "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/agent-detector/zipball/90694b9256099591cf9e55d08c18ba7a00bf099f", + "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f", + "shasum": "" + }, + "require": { + "php": "^8.2.0" + }, + "require-dev": { + "laravel/pint": "^1.24.0", + "pestphp/pest": "^3.8.5|^4.1.0", + "pestphp/pest-plugin-type-coverage": "^3.0|^4.0.2", + "phpstan/phpstan": "^2.1.26", + "rector/rector": "^2.1.7", + "symfony/var-dumper": "^7.3.3" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Laravel\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Detect if code is running in an AI agent or automated development environment", + "homepage": "https://github.com/laravel/agent-detector", + "keywords": [ + "Agent", + "ai", + "automation", + "claude", + "cursor", + "detection", + "devin", + "php" + ], + "support": { + "issues": "https://github.com/laravel/agent-detector/issues", + "source": "https://github.com/laravel/agent-detector" + }, + "time": "2026-04-29T18:32:34+00:00" + }, + { + "name": "laravel/boost", + "version": "v2.4.11", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "c98fc9d151de97a1864b51fc7c710fc03023ba17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/c98fc9d151de97a1864b51fc7c710fc03023ba17", + "reference": "c98fc9d151de97a1864b51fc7c710fc03023ba17", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "laravel/mcp": "^0.7.1|^0.8.0", + "laravel/prompts": "^0.3.10", + "laravel/roster": "^0.5.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.27.0", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", + "homepage": "https://github.com/laravel/boost", + "keywords": [ + "ai", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2026-06-26T08:21:49+00:00" + }, + { + "name": "laravel/mcp", + "version": "v0.8.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "0c32bf369c6432cab21458f9f4479da33a49ba37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/0c32bf369c6432cab21458f9f4479da33a49ba37", + "reference": "0c32bf369c6432cab21458f9f4479da33a49ba37", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2", + "symfony/process": "^7.4.5|^8.0.5" + }, + "require-dev": { + "laravel/pint": "^1.20", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.2.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2026-06-25T14:00:45+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-05-20T22:24:57+00:00" + }, + { + "name": "laravel/pao", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/pao.git", + "reference": "41b3c61ebeddce52a446afe6d21e0b02983fb2f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pao/zipball/41b3c61ebeddce52a446afe6d21e0b02983fb2f6", + "reference": "41b3c61ebeddce52a446afe6d21e0b02983fb2f6", + "shasum": "" + }, + "require": { + "laravel/agent-detector": "^2.0.2", + "php": "^8.3" + }, + "conflict": { + "laravel/framework": "<12.0.0", + "nunomaduro/collision": "<8.9.3", + "pestphp/pest": "<4.6.3 || >=6.0.0", + "phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.20.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench": "^10.11.0 || ^11.1.0", + "pestphp/pest": "^4.7.2 || ^5.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.0", + "phpstan/phpstan": "^2.2.2", + "rector/rector": "^2.4.5", + "symfony/process": "^7.4.8 || ^8.1.0", + "symfony/var-dumper": "^7.4.8 || ^8.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Laravel\\Pao\\Drivers\\Pest\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Laravel\\Pao\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Laravel\\Pao\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Agent-optimized output for PHP testing tools", + "keywords": [ + "Agent", + "PHPStan", + "ai", + "dev", + "paratest", + "pest", + "php", + "phpunit", + "rector", + "testing" + ], + "support": { + "issues": "https://github.com/laravel/pao/issues", + "source": "https://github.com/laravel/pao" + }, + "time": "2026-06-22T19:58:00+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.8", + "illuminate/view": "^12.62.0", + "larastan/larastan": "^3.10.0", + "laravel-zero/framework": "^12.1.0", + "laravel/agent-detector": "^2.0.2", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-06-16T15:34:04+00:00" + }, + { + "name": "laravel/roster", + "version": "v0.5.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/roster.git", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/yaml": "^7.2|^8.0" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.1", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2026-03-05T07:58:43+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.63.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "51bbce3f803c1d386cabbb44e618c955a12ff5fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/51bbce3f803c1d386cabbb44e618c955a12ff5fc", + "reference": "51bbce3f803c1d386cabbb44e618c955a12ff5fc", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-06-18T08:54:14+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.4", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-04-21T14:04:20+00:00" + }, + { + "name": "pestphp/pest", + "version": "v4.7.5", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", + "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.20.0", + "composer/xdebug-handler": "^3.0.5", + "nunomaduro/collision": "^8.9.4", + "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" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.30", + "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", + "psy/psysh": "^0.12.24" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Tia", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v4.7.5" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-07-06T17:06:29+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.3" + }, + "conflict": { + "pestphp/pest": "<4.0.0" + }, + "require-dev": { + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-08-20T12:35:58+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.7" + }, + "require-dev": { + "pestphp/pest": "^4.4.6", + "pestphp/pest-dev-tools": "^4.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-04-10T17:20:19+00:00" + }, + { + "name": "pestphp/pest-plugin-laravel", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-laravel.git", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0", + "shasum": "" + }, + "require": { + "laravel/framework": "^11.45.2|^12.52.0|^13.0", + "pestphp/pest": "^4.4.1", + "php": "^8.3.0" + }, + "require-dev": { + "laravel/dusk": "^8.3.6", + "orchestra/testbench": "^9.13.0|^10.9.0|^11.0", + "pestphp/pest-dev-tools": "^4.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Laravel\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Pest\\Laravel\\PestServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Laravel Plugin", + "keywords": [ + "framework", + "laravel", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-02-21T00:29:45+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + }, + "time": "2025-12-08T00:13:17+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.5", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0", + "reference": "909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-07-05T06:31:06+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "186dab580576598076de6818596d12b61801880e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "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", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.28" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-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 that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "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" + }, + "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-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-06-01T13:24:19+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "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" + }, + "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-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.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": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.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": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.30", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/900400a5b616d6fb306f9549f6da33ba615d3fbb", + "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "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", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "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" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-06-15T13:12:30+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-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 parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "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" + }, + "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/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "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" + }, + "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/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.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 calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "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" + }, + "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/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:40:20+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "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" + }, + "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/exporter", + "type": "tidelift" + } + ], + "time": "2026-05-20T04:37:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "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" + }, + "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/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:10:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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 counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "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" + }, + "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/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-05-19T16:22:07+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "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" + }, + "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/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "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" + }, + "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/type", + "type": "tidelift" + } + ], + "time": "2026-05-20T06:45:45+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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 that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v8.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "8e4cdd4311683516be06944f4b85244063cdb886" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8e4cdd4311683516be06944f4b85244063cdb886", + "reference": "8e4cdd4311683516be06944f4b85244063cdb886", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "yaml/yaml-test-suite": "*" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-09T11:06:24+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.7", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7" + }, + "time": "2026-02-17T17:25:14+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..1f8dd27 --- /dev/null +++ b/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')), + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache", "array" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..d7568ff --- /dev/null +++ b/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..d7eec61 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,136 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "storage", "octane", + | "session", "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'storage' => [ + 'driver' => 'storage', + 'disk' => env('CACHE_STORAGE_DISK'), + 'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + + /* + |-------------------------------------------------------------------------- + | Serializable Classes + |-------------------------------------------------------------------------- + | + | This value determines the classes that can be unserialized from cache + | storage. By default, no PHP classes will be unserialized from your + | cache to prevent gadget chain attacks if your APP_KEY is leaked. + | + */ + + 'serializable_classes' => false, + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..abbb88e --- /dev/null +++ b/config/database.php @@ -0,0 +1,184 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..6cfdfb3 --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim((string) env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/fortify.php b/config/fortify.php new file mode 100644 index 0000000..cedebce --- /dev/null +++ b/config/fortify.php @@ -0,0 +1,177 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Lowercase Usernames + |-------------------------------------------------------------------------- + | + | This value defines whether usernames should be lowercased before saving + | them in the database, as some database system string fields are case + | sensitive. You may disable this for your application if necessary. + | + */ + + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => '/dashboard', + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + 'passkeys' => 'passkeys', + ], + + /* + |-------------------------------------------------------------------------- + | Register View Routes + |-------------------------------------------------------------------------- + | + | Here you may specify if the routes returning views should be disabled as + | you may not need them when building your own application. This may be + | especially true if you're writing a custom single-page application. + | + */ + + 'views' => true, + + /* + |-------------------------------------------------------------------------- + | Passkeys + |-------------------------------------------------------------------------- + | + | These settings configure Fortify's passkey (WebAuthn) support. + | + */ + + 'passkeys' => [ + 'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST), + 'allowed_origins' => [config('app.url')], + 'user_handle_secret' => env('PASSKEYS_USER_HANDLE_SECRET', config('app.key')), + 'timeout' => 60000, + ], + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features, or you can even remove all of these if you need to. + | + */ + + 'features' => [ + Features::registration(), + Features::resetPasswords(), + Features::emailVerification(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0 + ]), + Features::passkeys([ + 'confirmPassword' => true, + ]), + ], + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..b09cb25 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..e32e88d --- /dev/null +++ b/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..ec226a4 --- /dev/null +++ b/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..e0c0eac --- /dev/null +++ b/config/session.php @@ -0,0 +1,233 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session', + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + + /* + |-------------------------------------------------------------------------- + | Session Serialization + |-------------------------------------------------------------------------- + | + | This value controls the serialization strategy for session data, which + | is JSON by default. Setting this to "php" allows the storage of PHP + | objects in the session but can make an application vulnerable to + | "gadget chain" serialization attacks if the APP_KEY is leaked. + | + | Supported: "json", "php" + | + */ + + 'serialization' => 'json', + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php new file mode 100644 index 0000000..52ff60f --- /dev/null +++ b/database/factories/CategoryFactory.php @@ -0,0 +1,24 @@ + + */ +class CategoryFactory extends Factory +{ + /** + * @return array + */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'name' => fake()->unique()->words(2, true), + ]; + } +} diff --git a/database/factories/CertificateFactory.php b/database/factories/CertificateFactory.php new file mode 100644 index 0000000..8abd36e --- /dev/null +++ b/database/factories/CertificateFactory.php @@ -0,0 +1,45 @@ + + */ +class CertificateFactory extends Factory +{ + /** + * @return array + */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'category_id' => null, + 'title' => fake()->sentence(3), + 'issuer' => fake()->company(), + 'certificate_number' => fake()->bothify('CERT-####-????'), + 'issued_at' => fake()->dateTimeBetween('-2 years', '-1 month'), + 'file_path' => null, + 'expires_at' => fake()->dateTimeBetween('+2 months', '+2 years'), + 'notes' => null, + ]; + } + + public function expired(): static + { + return $this->state(fn (array $attributes) => [ + 'expires_at' => fake()->dateTimeBetween('-2 years', '-1 day'), + ]); + } + + public function expiringSoon(): static + { + return $this->state(fn (array $attributes) => [ + 'expires_at' => today()->addWeeks(2), + ]); + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..98825c8 --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,60 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + 'two_factor_secret' => null, + 'two_factor_recovery_codes' => null, + 'two_factor_confirmed_at' => null, + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } + + /** + * Indicate that the model has two-factor authentication configured. + */ + public function withTwoFactor(): static + { + return $this->state(fn (array $attributes) => [ + 'two_factor_secret' => encrypt('secret'), + 'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])), + 'two_factor_confirmed_at' => now(), + ]); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..06dc7a5 --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->bigInteger('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->bigInteger('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..edac6fe --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,59 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedSmallInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->string('connection'); + $table->string('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + + $table->index(['connection', 'queue', 'failed_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/migrations/2024_01_01_000000_create_passkeys_table.php b/database/migrations/2024_01_01_000000_create_passkeys_table.php new file mode 100644 index 0000000..cb884c3 --- /dev/null +++ b/database/migrations/2024_01_01_000000_create_passkeys_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('credential_id')->unique(); + $table->json('credential'); + $table->timestamp('last_used_at')->nullable(); + $table->timestamps(); + + $table->index('user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('passkeys'); + } +}; diff --git a/database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php b/database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..187d974 --- /dev/null +++ b/database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php @@ -0,0 +1,34 @@ +text('two_factor_secret')->after('password')->nullable(); + $table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable(); + $table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]); + }); + } +}; diff --git a/database/migrations/2026_07_08_000001_create_categories_table.php b/database/migrations/2026_07_08_000001_create_categories_table.php new file mode 100644 index 0000000..dd34d0f --- /dev/null +++ b/database/migrations/2026_07_08_000001_create_categories_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->timestamps(); + + $table->unique(['user_id', 'name']); + }); + } + + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2026_07_08_000002_create_certificates_table.php b/database/migrations/2026_07_08_000002_create_certificates_table.php new file mode 100644 index 0000000..ae69f73 --- /dev/null +++ b/database/migrations/2026_07_08_000002_create_certificates_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete(); + $table->string('title'); + $table->string('file_path')->nullable(); + $table->date('expires_at'); + $table->timestamps(); + + $table->index(['user_id', 'expires_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('certificates'); + } +}; diff --git a/database/migrations/2026_07_08_000003_add_certificate_settings_to_users_table.php b/database/migrations/2026_07_08_000003_add_certificate_settings_to_users_table.php new file mode 100644 index 0000000..a77ad4b --- /dev/null +++ b/database/migrations/2026_07_08_000003_add_certificate_settings_to_users_table.php @@ -0,0 +1,24 @@ +integer('reminder_months_before')->default(1)->after('remember_token'); + $table->string('ocr_provider')->default('none')->after('reminder_months_before'); + $table->text('ocr_api_key')->nullable()->after('ocr_provider'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['reminder_months_before', 'ocr_provider', 'ocr_api_key']); + }); + } +}; diff --git a/database/migrations/2026_07_08_000004_add_last_reminded_at_to_certificates_table.php b/database/migrations/2026_07_08_000004_add_last_reminded_at_to_certificates_table.php new file mode 100644 index 0000000..867e9f9 --- /dev/null +++ b/database/migrations/2026_07_08_000004_add_last_reminded_at_to_certificates_table.php @@ -0,0 +1,22 @@ +timestamp('last_reminded_at')->nullable()->after('expires_at'); + }); + } + + public function down(): void + { + Schema::table('certificates', function (Blueprint $table) { + $table->dropColumn('last_reminded_at'); + }); + } +}; diff --git a/database/migrations/2026_07_09_000001_add_details_to_certificates_table.php b/database/migrations/2026_07_09_000001_add_details_to_certificates_table.php new file mode 100644 index 0000000..d77038c --- /dev/null +++ b/database/migrations/2026_07_09_000001_add_details_to_certificates_table.php @@ -0,0 +1,25 @@ +string('issuer')->nullable()->after('title'); + $table->string('certificate_number')->nullable()->after('issuer'); + $table->date('issued_at')->nullable()->after('certificate_number'); + $table->text('notes')->nullable()->after('expires_at'); + }); + } + + public function down(): void + { + Schema::table('certificates', function (Blueprint $table) { + $table->dropColumn(['issuer', 'certificate_number', 'issued_at', 'notes']); + }); + } +}; diff --git a/database/migrations/2026_07_10_000001_add_locale_to_users_table.php b/database/migrations/2026_07_10_000001_add_locale_to_users_table.php new file mode 100644 index 0000000..edb7906 --- /dev/null +++ b/database/migrations/2026_07_10_000001_add_locale_to_users_table.php @@ -0,0 +1,22 @@ +string('locale', 5)->default('en')->after('ocr_api_key'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('locale'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..6b901f8 --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,25 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/lang/nl.json b/lang/nl.json new file mode 100644 index 0000000..a608c27 --- /dev/null +++ b/lang/nl.json @@ -0,0 +1,224 @@ +{ + "\":name\" will be deleted. Certificates in this category are kept and become uncategorized.": "\":name\" wordt verwijderd. Certificaten in deze categorie blijven behouden en worden ongecategoriseerd.", + "\":title\" and its stored file will be permanently deleted.": "\":title\" en het bijbehorende bestand worden permanent verwijderd.", + "2FA recovery codes": "2FA-herstelcodes", + ":name has shared certificates with you as a password-protected ZIP archive attached to this email.": ":name heeft certificaten met je gedeeld als een met wachtwoord beveiligd ZIP-bestand, bijgevoegd bij deze e-mail.", + ":name has shared the following certificates with you.": ":name heeft de volgende certificaten met je gedeeld.", + ":name shared certificates with you": ":name heeft certificaten met je gedeeld", + "A new verification link has been sent to the email address you provided during registration.": "Er is een nieuwe verificatielink verstuurd naar het e-mailadres dat je bij registratie hebt opgegeven.", + "A new verification link has been sent to your email address.": "Er is een nieuwe verificatielink naar je e-mailadres verstuurd.", + "API key": "API-sleutel", + "Add": "Toevoegen", + "Add a passkey to sign in without a password": "Voeg een passkey toe om zonder wachtwoord in te loggen", + "Add certificate": "Certificaat toevoegen", + "Add passkey": "Passkey toevoegen", + "Add your first certificate": "Voeg je eerste certificaat toe", + "Add your first certificate to get started.": "Voeg je eerste certificaat toe om te beginnen.", + "Added :time": "Toegevoegd :time", + "All categories": "Alle categorieën", + "Already have an account?": "Heb je al een account?", + "An overview of your certificates and their expiry status.": "Een overzicht van je certificaten en hun vervalstatus.", + "Appearance": "Weergave", + "Appearance settings": "Weergave-instellingen", + "Archive password": "Archiefwachtwoord", + "Are you sure you want to delete your account?": "Weet je zeker dat je je account wilt verwijderen?", + "Are you sure you want to remove the passkey \":name\"? You will no longer be able to use it to sign in.": "Weet je zeker dat je de passkey \":name\" wilt verwijderen? Je kunt hem daarna niet meer gebruiken om in te loggen.", + "Authenticating...": "Bezig met authenticeren...", + "Authentication code": "Authenticatiecode", + "Back": "Terug", + "By category": "Per categorie", + "Cancel": "Annuleren", + "Categories": "Categorieën", + "Category": "Categorie", + "Category created.": "Categorie aangemaakt.", + "Category deleted. Its certificates are now uncategorized.": "Categorie verwijderd. De certificaten zijn nu ongecategoriseerd.", + "Certificate added.": "Certificaat toegevoegd.", + "Certificate deleted.": "Certificaat verwijderd.", + "Certificate expires today: :title": "Certificaat verloopt vandaag: :title", + "Certificate expiring soon: :title": "Certificaat verloopt binnenkort: :title", + "Certificate number": "Certificaatnummer", + "Certificate settings updated.": "Certificaatinstellingen bijgewerkt.", + "Certificate updated.": "Certificaat bijgewerkt.", + "Certificates": "Certificaten", + "Certificates shared with you": "Certificaten met je gedeeld", + "Click here to re-send the verification email.": "Klik hier om de verificatie-e-mail opnieuw te versturen.", + "Close": "Sluiten", + "Configure expiry reminders and document scanning": "Stel vervalherinneringen en documentscannen in", + "Confirm": "Bevestigen", + "Confirm password": "Wachtwoord bevestigen", + "Confirm with passkey": "Bevestig met passkey", + "Confirming...": "Bezig met bevestigen...", + "Continue": "Doorgaan", + "Create account": "Account aanmaken", + "Create an account": "Maak een account aan", + "Create your first category to organize your certificates.": "Maak je eerste categorie aan om je certificaten te organiseren.", + "Current password": "Huidig wachtwoord", + "Dark": "Donker", + "Delete": "Verwijderen", + "Delete account": "Account verwijderen", + "Delete category?": "Categorie verwijderen?", + "Delete certificate?": "Certificaat verwijderen?", + "Delete your account and all of its resources": "Verwijder je account en alle bijbehorende gegevens", + "Disable 2FA": "2FA uitschakelen", + "Document preview": "Documentvoorbeeld", + "Document scanning (OCR)": "Documentscannen (OCR)", + "Documentation": "Documentatie", + "Don't have an account?": "Nog geen account?", + "Done": "Klaar", + "Download": "Downloaden", + "Download current file": "Huidig bestand downloaden", + "Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate codes above.": "Elke herstelcode kan één keer worden gebruikt om toegang te krijgen tot je account en wordt na gebruik verwijderd. Heb je meer nodig, klik dan hierboven op Codes opnieuw genereren.", + "Edit": "Bewerken", + "Edit certificate": "Certificaat bewerken", + "Email": "E-mail", + "Email address": "E-mailadres", + "Email password reset link": "Verstuur link om wachtwoord opnieuw in te stellen", + "Email verification": "E-mailverificatie", + "Enable 2FA": "2FA inschakelen", + "Enable two-factor authentication": "Tweestapsverificatie inschakelen", + "Ensure your account is using a long, random password to stay secure": "Zorg dat je account een lang, willekeurig wachtwoord gebruikt om veilig te blijven", + "Enter the 6-digit code from your authenticator app.": "Voer de 6-cijferige code uit je authenticator-app in.", + "Enter the authentication code provided by your authenticator application.": "Voer de authenticatiecode in die door je authenticator-app wordt gegeven.", + "Enter your API key": "Voer je API-sleutel in", + "Enter your details below to create your account": "Vul hieronder je gegevens in om je account aan te maken", + "Enter your email and password below to log in": "Vul hieronder je e-mailadres en wachtwoord in om in te loggen", + "Enter your email to receive a password reset link": "Vul je e-mailadres in om een link te ontvangen om je wachtwoord opnieuw in te stellen", + "Expired": "Verlopen", + "Expires": "Vervalt", + "Expiring soon": "Vervalt binnenkort", + "Expiry date": "Vervaldatum", + "For your security, the password is sent separately through another channel (for example SMS or WhatsApp). You will need it to open the archive.": "Voor je veiligheid wordt het wachtwoord apart via een ander kanaal verstuurd (bijvoorbeeld sms of WhatsApp). Je hebt het nodig om het archief te openen.", + "Forgot password": "Wachtwoord vergeten", + "Forgot your password?": "Wachtwoord vergeten?", + "Full name": "Volledige naam", + "Give this passkey a name to help you identify it later.": "Geef deze passkey een naam zodat je hem later herkent.", + "Hello :name,": "Hallo :name,", + "Here is the status of your certificates.": "Dit is de status van je certificaten.", + "Hide recovery codes": "Herstelcodes verbergen", + "If you did not expect this email, you can safely ignore it.": "Als je deze e-mail niet verwachtte, kun je hem veilig negeren.", + "Issue date": "Uitgiftedatum", + "Issuing authority": "Uitgevende instantie", + "Last used :time": "Laatst gebruikt :time", + "Leave blank to keep the current key": "Laat leeg om de huidige sleutel te behouden", + "Light": "Licht", + "Log in": "Inloggen", + "Log in to your account": "Log in op je account", + "Log out": "Uitloggen", + "Manage": "Beheren", + "Manage your passkeys for passwordless sign-in": "Beheer je passkeys voor inloggen zonder wachtwoord", + "Manage your profile and account settings": "Beheer je profiel- en accountinstellingen", + "Manage your two-factor authentication settings": "Beheer je instellingen voor tweestapsverificatie", + "Method": "Methode", + "Name": "Naam", + "Needs attention": "Actie vereist", + "New category name": "Naam nieuwe categorie", + "New password": "Nieuw wachtwoord", + "No categories yet": "Nog geen categorieën", + "No categories yet.": "Nog geen categorieën.", + "No certificates found": "Geen certificaten gevonden", + "No certificates in this category. Try another filter.": "Geen certificaten in deze categorie. Probeer een ander filter.", + "No certificates yet": "Nog geen certificaten", + "No passkeys yet": "Nog geen passkeys", + "None (manual entry)": "Geen (handmatig invoeren)", + "Notes": "Notities", + "Nothing needs attention. All your certificates are up to date.": "Niets vereist actie. Al je certificaten zijn up-to-date.", + "Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "Zodra je account is verwijderd, worden alle bijbehorende gegevens permanent verwijderd. Voer je wachtwoord in om te bevestigen dat je je account definitief wilt verwijderen.", + "Optional notes about this certificate": "Optionele notities over dit certificaat", + "Optional. Leave empty to keep the current file.": "Optioneel. Laat leeg om het huidige bestand te behouden.", + "Or confirm with password": "Of bevestig met wachtwoord", + "Or continue with email": "Of ga verder met e-mail", + "Or, return to": "Of ga terug naar", + "Organize your certificates with personal categories.": "Organiseer je certificaten met persoonlijke categorieën.", + "PDF or image, up to 10 MB.": "PDF of afbeelding, tot 10 MB.", + "Passkey name": "Naam passkey", + "Passkeys": "Passkeys", + "Passkeys are not supported in this browser.": "Passkeys worden niet ondersteund in deze browser.", + "Password": "Wachtwoord", + "Password updated.": "Wachtwoord bijgewerkt.", + "Password-protected ZIP attachment": "Met wachtwoord beveiligde ZIP-bijlage", + "Please confirm access to your account by entering one of your emergency recovery codes.": "Bevestig toegang tot je account door een van je noodherstelcodes in te voeren.", + "Please enter your new password below": "Voer hieronder je nieuwe wachtwoord in", + "Please renew it in good time to stay compliant.": "Verleng het op tijd om compliant te blijven.", + "Please verify your email address by clicking on the link we just emailed to you.": "Verifieer je e-mailadres door op de link te klikken die we je zojuist per e-mail hebben gestuurd.", + "Profile": "Profiel", + "Profile settings": "Profielinstellingen", + "Profile updated.": "Profiel bijgewerkt.", + "Recently added": "Recent toegevoegd", + "Recipient email": "E-mailadres ontvanger", + "Recovery code": "Herstelcode", + "Recovery codes": "Herstelcodes", + "Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.": "Met herstelcodes krijg je weer toegang als je je 2FA-apparaat kwijtraakt. Bewaar ze in een veilige wachtwoordmanager.", + "Regenerate codes": "Codes opnieuw genereren", + "Register": "Registreren", + "Register passkey": "Passkey registreren", + "Registering...": "Bezig met registreren...", + "Remember me": "Onthoud mij", + "Remind me this many months before expiry": "Herinner mij dit aantal maanden voor vervaldatum", + "Remove passkey": "Passkey verwijderen", + "Renew it as soon as possible to avoid a lapse in compliance.": "Verleng het zo snel mogelijk om een onderbreking in compliance te voorkomen.", + "Replace document": "Document vervangen", + "Resend verification email": "Verificatie-e-mail opnieuw versturen", + "Reset password": "Wachtwoord opnieuw instellen", + "Save": "Opslaan", + "Save certificate": "Certificaat opslaan", + "Save changes": "Wijzigingen opslaan", + "Scanning document for suggestions…": "Document wordt gescand voor suggesties…", + "Search": "Zoeken", + "Secure link (valid 48 hours)": "Beveiligde link (48 uur geldig)", + "Secure links sent to :email.": "Beveiligde links verzonden naar :email.", + "Security": "Beveiliging", + "Security settings": "Beveiligingsinstellingen", + "Select a document to preview it here.": "Selecteer een document om het hier te bekijken.", + "Send": "Versturen", + "Send the selected certificates to an external recipient.": "Verstuur de geselecteerde certificaten naar een externe ontvanger.", + "Settings": "Instellingen", + "Share": "Delen", + "Share certificates": "Certificaten delen", + "Sign in with a passkey": "Inloggen met een passkey", + "Sign up": "Registreren", + "System": "Systeem", + "Thanks": "Met vriendelijke groet", + "The ZIP was emailed. Share this password with the recipient through a different channel (SMS or WhatsApp). It will not be shown again.": "De ZIP is verstuurd per e-mail. Deel dit wachtwoord met de ontvanger via een ander kanaal (sms of WhatsApp). Het wordt niet opnieuw getoond.", + "The selected certificates have no files to share.": "De geselecteerde certificaten hebben geen bestanden om te delen.", + "These links are valid until :date, after which they will stop working.": "Deze links zijn geldig tot :date, daarna werken ze niet meer.", + "This is a secure area of the application. Please confirm your password before continuing.": "Dit is een beveiligd gedeelte van de applicatie. Bevestig je wachtwoord voordat je verdergaat.", + "Title": "Titel", + "To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app.": "Scan de QR-code of voer de installatiesleutel in je authenticator-app in om tweestapsverificatie te voltooien.", + "Too many share attempts. Please try again later.": "Te veel deelpogingen. Probeer het later opnieuw.", + "Total": "Totaal", + "Two-factor authentication": "Tweestapsverificatie", + "Two-factor authentication enabled": "Tweestapsverificatie ingeschakeld", + "Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.": "Tweestapsverificatie is nu ingeschakeld. Scan de QR-code of voer de installatiesleutel in je authenticator-app in.", + "Uncategorized": "Ongecategoriseerd", + "Update password": "Wachtwoord bijwerken", + "Update the appearance settings for your account": "Werk de weergave-instellingen van je account bij", + "Update the details or replace the stored document.": "Werk de gegevens bij of vervang het opgeslagen document.", + "Update your name and email address": "Werk je naam en e-mailadres bij", + "Upload a document to preview it, then confirm the details — pre-filled automatically if scanning is enabled.": "Upload een document om het te bekijken en bevestig daarna de gegevens — automatisch ingevuld als scannen is ingeschakeld.", + "Upload your first certificate to start tracking issue dates, expiry, and reminders.": "Upload je eerste certificaat om uitgiftedatums, vervaldatums en herinneringen bij te houden.", + "Uploading…": "Uploaden…", + "Valid": "Geldig", + "Verify authentication code": "Authenticatiecode verifiëren", + "View all": "Alles bekijken", + "View recovery codes": "Herstelcodes bekijken", + "View your certificates": "Bekijk je certificaten", + "Welcome": "Welkom", + "Welcome back, :name": "Welkom terug, :name", + "When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.": "Wanneer je tweestapsverificatie inschakelt, wordt tijdens het inloggen om een beveiligde pincode gevraagd. Deze pincode kun je ophalen uit een TOTP-ondersteunende app op je telefoon.", + "You will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.": "Tijdens het inloggen wordt om een beveiligde, willekeurige pincode gevraagd, die je kunt ophalen uit de TOTP-ondersteunende app op je telefoon.", + "Your certificate \":title\" expires today.": "Je certificaat \":title\" verloopt vandaag.", + "Your certificate \":title\" will expire on :date.": "Je certificaat \":title\" verloopt op :date.", + "Your email address is unverified.": "Je e-mailadres is niet geverifieerd.", + "e.g. VCA, Red Cross": "bijv. VCA, Rode Kruis", + "e.g., MacBook Pro, iPhone": "bijv. MacBook Pro, iPhone", + "log in": "inloggen", + "login using a recovery code": "inloggen met een herstelcode", + "login using an authentication code": "inloggen met een authenticatiecode", + "or you can": "of je kunt", + "or, enter the code manually": "of voer de code handmatig in", + ":count certificate selected|:count certificates selected": ":count certificaat geselecteerd|:count certificaten geselecteerd", + "Language": "Taal", + "Choose the language used throughout the application": "Kies de taal die in de hele applicatie wordt gebruikt", + "English": "Engels", + "Dutch": "Nederlands" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3a15099 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1600 @@ +{ + "name": "Certified", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@alpinejs/collapse": "^3.15.12", + "@laravel/passkeys": "^0.2.0", + "@tailwindcss/vite": "^4.1.11", + "alpinejs": "^3.15.12", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^3.1", + "tailwindcss": "^4.0.7", + "vite": "^8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.9.5", + "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", + "lightningcss-linux-x64-gnu": "^1.29.1" + } + }, + "node_modules/@alpinejs/collapse": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.15.12.tgz", + "integrity": "sha512-BKNANLtNXuWYOSAnajSKLPjTsmHRNrv0ALFTbpmqt2/klHFooPhctSwkhFVPQb7rZ8BjEKHmNaBwnSbgtpk6xg==", + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@laravel/passkeys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@laravel/passkeys/-/passkeys-0.2.0.tgz", + "integrity": "sha512-K+owv26weKrYaoTJutqU40UsJZpwYkqB9jdlz5Bv/C54nxj2+Py9jbVwxASWg21fLsPhn3ngPKZE5RnfakCQKg==", + "license": "MIT", + "dependencies": { + "@simplewebauthn/browser": "^13.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "svelte": "^5.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", + "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@simplewebauthn/browser": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "license": "MIT" + }, + "node_modules/alpinejs": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz", + "integrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.3.tgz", + "integrity": "sha512-ihjs0E2SxvDgq/MK418hX6YycQgKhsqxpbZuZbHo0yKfqDWdymWMjWYIpCIzqDDLLKClHlXev8whW/8WXmJ0BA==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.1.0.tgz", + "integrity": "sha512-Fzocl+X4eQ9jOi0RwdphYRGkUbPJ3ky1pTAST5Ot18cS2gw6d2vldK2eCrlKDVjtibCjCx5qptYDlA0373n7qg==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "tinyglobby": "^0.2.12", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "fontaine": "^0.5.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "fontaine": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "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/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "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" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/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==", + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b8e022 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "@alpinejs/collapse": "^3.15.12", + "@laravel/passkeys": "^0.2.0", + "@tailwindcss/vite": "^4.1.11", + "alpinejs": "^3.15.12", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^3.1", + "tailwindcss": "^4.0.7", + "vite": "^8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.9.5", + "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", + "lightningcss-linux-x64-gnu": "^1.29.1" + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..1ca23a4 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +includes: + - vendor/larastan/larastan/extension.neon + - vendor/nesbot/carbon/extension.neon + +parameters: + paths: + - app/ + - bootstrap/app.php + - config/ + - database/ + - routes/ + + level: 7 diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e7f0a48 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,36 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..93061b6 --- /dev/null +++ b/pint.json @@ -0,0 +1,3 @@ +{ + "preset": "laravel" +} diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c2efef6de4482ad4ec770787891fb3ab7f7b1f58 GIT binary patch literal 1662 zcmV-^27&pBP)40{{R3XI2D70000&P)t-s00030 zFd+XeApb8Q|1BW@E+GFcApbBR|1Kc^Eg=6bApb5P|1Kc^EFk|aApa~N|1BW@Eg=6e zApb5P|1BW@Eg&n7Ql0<+02OpnPE!CNzd)dXU=ZKm?|<)rkZ`ZhFmF)fpgb%900q`b zL_t(|+U%Oqj+-zLMaMYBBrGf>;s1ZNY7`jIiI_O8!Rm4!Tgf`>xtSR*$bTdkB!69t zVK_O-TcI8P=u%70x?P5+&ZXoke;#Hk@~pfohJ~`UXs9Q;_B2)gTk?{cvbc)et1K<* zr+Qaj9xAKsDzB#eYQpr7r@Sm0>dVCQfKCB0Kf{y|EP9n~<+TZwVye09V$sm9n9{PSY^C}VzXiE}GC#|%c)PWA z!&5y>`JP4VkDV6b8$Z_jJJu@A$QeFse^IT?qj=G4l>4y#(Kj|Guiq{iFk{=Fwl*76 z)xQ4|>IRkjy#1lI8JM_!yJWG>8ZJNf8dXXx zrk~GbPG4tWE+u0Q(BCgl#w@cPJ5nx)T9MuG26Ggy6OkTfT zvPjI^>P)^G7FA5^`aMfVOxu)|x2A9549ulPjgm!RI{t-}!yN(BSbnfZ0rTAmB^Yxa z0ShsC{k=0v223kLF@%7Hn5rZ4wL=GnpitveraoqSfe(_EIcO5du!CjnDf@wtxv3e z-u^bn6xYI{>k~_4b4*0{)`LMCWEV`GECi(~y~rMz#`428YDY}1JOdWj&DW^?F-Ib; zJl{lRfw{1#A0resWj2_y2v`;oie!a3x9HrIxnXL+G9OMEVvZJ7Q)Y@O(#)P#Gsb*Y zz6+8!=6Lpjd0-xx2j+o!U>=wU<`2TWpY@?U`fOKhgXs$-hQ`^*57T3Y7GFKsXEUm) zBmpLJD?F4LXCptnrod!)-s4>q&NlMH1ep8=0k2DW_S$RM1ei2s8;}?pXCpsMfQj(j zNDOf{^1}p}D=)N>81ii9hY2uO&CkS;XEQ%chDr0UB!)N}`C$S~^BV*vhRd^&A11-1 z`R$K=!+176!AgetySX#KX&8o~XtND<+Gs@?_dhEVl?dUf^KAF4`3KmZ#1o5M$8F`` z3P!Tl&&CLr3Ul6+|GA>4A2dj*!Zf~z9YX~DLkTA3XY8i74zs-cJ)DTceLfYYA-}08 zCfNm;#x)M#D5naO^M{f7;@Z>}U}BCre7ypnoy1IDM_|xHKkNy25-bsNjhURKb+p6ga^RJ>eNU6hI2E^Ow5A+W|JeZDC8Wqj7Y8Fr9@Uvc=cY`i& zDS4WCQI*lV*os&Gro@tzcjHTqy#y%$-R|Mc0;Z=P9R-{!<>DZ*bg~pkU*-o&W#<07*qo IM6N<$f;hnhc>n+a literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..236fadb94188e9189807c50aa8480b3070a0cc8d GIT binary patch literal 4286 zcmcJSYiv|S6vqb^Sj9>!kD_?#USEjFuB*k05m`ulKhQ)0{ZR3Nj~ES!#h9qrHiD>$ zFCbA83#r6d--z!Q-dZre@B@j4kBxrw(=r7ZdN&YYP!=ggUN z&s>a|z(3zN`fK`{jA=H;%m+kf8A$GajE&{z=dAJbc7mU^S=a@A(CVLujIm?&L%;DC zLe@6JdI;b*I0WCqqi~9wGmd_s)3`n7>{QyT;Y-k-FM>{Z23kR}*bD8AB1Y}dT<%4|D*yFG{ipz1dZ-md_9asjFVAL*Xjeow=eI@kwWU_SmSU;ft*cgCUqSjPbCwmKa-bR^DI`k=CZ?Ca;^-d5Ll<&7I3$q~lW zumU~>#c()c$ zznq=I*j4Zy`~=s7e6N}x_jxK`Zh-@E10+?J+sM~|@>qVCnUcYu{D&HP^q=+0M}32-cM_!3Ciyi+W;gJQ1O$7`Uq{%qEf zQ&BAEGXHHD1l564qg*VjMHc7h8MJr9F#HPgp`07>0%S*IU-OwO9s`}T@>+3T1B!b* zKArZ1un%4W-3_mTYTL(f9k{v?=^U+ny>m|ylSw>PH%q64pG5ofa4((kv;&-?dWdAkLy`uhG@CDoiOQ8?+ z9kmLbnp3s^M6T<5NBhy**WqFab58X;{Fc}ICS9`EhD_t_k?aMYs+oF+X2x}$w)C!s zub>y+giUTd`u=o2$VPbg%w}A5K=FGP&VnVNeaN?!pjv4BMJ}c{)BXz-le3+UDBqmk zP^aQH6SlzLumSXcGEJ%aDelxC<@f^F20w!KrQDna?i}Mq^k;zX6#3f)bNvO(k)BF^ zHzB_V0(cothr0F?dSz!aw81>*Q(|8=^W<+HdO$YTVt*Rwysv=|;RCn=q%&SVZcpiq z>|ITJ5A23ruovR@SbG{bo3)A(@mAf`d#C(4&3${+iCyjbvGi?Vt?lZx!zN>grl)PO zquU0hL7OTS3ys0yLdGmDv^2C7yp|~iFVkJ{(t`!Bd#K + + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..88845e0 --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,128 @@ +@import 'tailwindcss'; +@import '../../vendor/livewire/flux/dist/flux.css'; + +@source '../views'; +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../vendor/livewire/flux-pro/stubs/**/*.blade.php'; +@source '../../vendor/livewire/flux/stubs/**/*.blade.php'; + +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + + --color-zinc-50: #fafafa; + --color-zinc-100: #f5f5f5; + --color-zinc-200: #e5e5e5; + --color-zinc-300: #d4d4d4; + --color-zinc-400: #a3a3a3; + --color-zinc-500: #737373; + --color-zinc-600: #525252; + --color-zinc-700: #404040; + --color-zinc-800: #262626; + --color-zinc-900: #171717; + --color-zinc-950: #0a0a0a; + + --color-accent: var(--color-neutral-800); + --color-accent-content: var(--color-neutral-800); + --color-accent-foreground: var(--color-white); + + /* Marketing pages */ + --animate-fade-in-up: fade-in-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) both; + --animate-scan-beam: scan-beam 1.6s ease-in-out infinite; + --animate-caret-blink: caret-blink 1s steps(2) infinite; + --animate-float-slow: float-slow 7s ease-in-out infinite; + + @keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(1.5rem); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes scan-beam { + 0%, + 100% { + top: 0%; + } + 50% { + top: calc(100% - 3px); + } + } + + @keyframes caret-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } + } + + @keyframes float-slow { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-0.75rem); + } + } +} + +[x-cloak] { + display: none !important; +} + +@utility bg-grid-slate { + background-image: + linear-gradient(to right, --alpha(var(--color-slate-400) / 14%) 1px, transparent 1px), + linear-gradient(to bottom, --alpha(var(--color-slate-400) / 14%) 1px, transparent 1px); + background-size: 2.5rem 2.5rem; +} + +@utility bg-noise { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E"); +} + +@layer theme { + .dark { + --color-accent: var(--color-white); + --color-accent-content: var(--color-white); + --color-accent-foreground: var(--color-neutral-800); + } +} + +@layer base { + + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } +} + +[data-flux-field]:not(ui-radio, ui-checkbox) { + @apply grid gap-2; +} + +[data-flux-label] { + @apply !mb-0 !leading-tight; +} + +input:focus[data-flux-control], +textarea:focus[data-flux-control], +select:focus[data-flux-control] { + @apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground; +} + +/* \[:where(&)\]:size-4 { + @apply size-4; +} */ diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..e69de29 diff --git a/resources/js/marketing.js b/resources/js/marketing.js new file mode 100644 index 0000000..05e80f8 --- /dev/null +++ b/resources/js/marketing.js @@ -0,0 +1,42 @@ +import Alpine from 'alpinejs'; +import collapse from '@alpinejs/collapse'; + +Alpine.plugin(collapse); + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +// Interactive OCR scan demo in the features section. +Alpine.data('ocrDemo', () => ({ + state: 'idle', // idle | scanning | typing | done + progress: 0, + fields: { title: '', issuer: '', expires: '' }, + result: { title: 'VCA Basisveiligheid (B-VCA)', issuer: 'SSVV — VCA Infra', expires: '12-03-2029' }, + + async scan() { + if (this.state !== 'idle' && this.state !== 'done') return; + + this.state = 'scanning'; + this.progress = 0; + this.fields = { title: '', issuer: '', expires: '' }; + + while (this.progress < 100) { + this.progress = Math.min(100, this.progress + Math.random() * 14 + 4); + await sleep(120); + } + + this.state = 'typing'; + + for (const key of Object.keys(this.result)) { + for (const char of this.result[key]) { + this.fields[key] += char; + await sleep(28); + } + await sleep(180); + } + + this.state = 'done'; + }, +})); + +window.Alpine = Alpine; +Alpine.start(); diff --git a/resources/js/passkeys.js b/resources/js/passkeys.js new file mode 100644 index 0000000..af3935e --- /dev/null +++ b/resources/js/passkeys.js @@ -0,0 +1,4 @@ +import { Passkeys } from '@laravel/passkeys'; + +window.Passkeys = Passkeys; +window.dispatchEvent(new CustomEvent('passkeys:ready')); diff --git a/resources/views/components/app-logo-icon.blade.php b/resources/views/components/app-logo-icon.blade.php new file mode 100644 index 0000000..0adc3a2 --- /dev/null +++ b/resources/views/components/app-logo-icon.blade.php @@ -0,0 +1,8 @@ + + + diff --git a/resources/views/components/app-logo.blade.php b/resources/views/components/app-logo.blade.php new file mode 100644 index 0000000..26e8f68 --- /dev/null +++ b/resources/views/components/app-logo.blade.php @@ -0,0 +1,17 @@ +@props([ + 'sidebar' => false, +]) + +@if($sidebar) + + + + + +@else + + + + + +@endif diff --git a/resources/views/components/auth-header.blade.php b/resources/views/components/auth-header.blade.php new file mode 100644 index 0000000..e596a3f --- /dev/null +++ b/resources/views/components/auth-header.blade.php @@ -0,0 +1,9 @@ +@props([ + 'title', + 'description', +]) + +
+ {{ $title }} + {{ $description }} +
diff --git a/resources/views/components/auth-session-status.blade.php b/resources/views/components/auth-session-status.blade.php new file mode 100644 index 0000000..98e0011 --- /dev/null +++ b/resources/views/components/auth-session-status.blade.php @@ -0,0 +1,9 @@ +@props([ + 'status', +]) + +@if ($status) +
merge(['class' => 'font-medium text-sm text-green-600']) }}> + {{ $status }} +
+@endif diff --git a/resources/views/components/desktop-user-menu.blade.php b/resources/views/components/desktop-user-menu.blade.php new file mode 100644 index 0000000..7fb3706 --- /dev/null +++ b/resources/views/components/desktop-user-menu.blade.php @@ -0,0 +1,39 @@ + + + + +
+ +
+ {{ auth()->user()->name }} + {{ auth()->user()->email }} +
+
+ + + + {{ __('Settings') }} + +
+ @csrf + + {{ __('Log out') }} + +
+
+
+
diff --git a/resources/views/components/language-switcher.blade.php b/resources/views/components/language-switcher.blade.php new file mode 100644 index 0000000..0e73dce --- /dev/null +++ b/resources/views/components/language-switcher.blade.php @@ -0,0 +1,24 @@ +@props(['sidebar' => false]) + + + @if ($sidebar) + + {{ \App\Enums\Locale::from(app()->getLocale())->label() }} + + @else + + {{ strtoupper(app()->getLocale()) }} + + @endif + + + @foreach (\App\Enums\Locale::cases() as $locale) + + {{ $locale->label() }} + + @endforeach + + diff --git a/resources/views/components/passkey-registration.blade.php b/resources/views/components/passkey-registration.blade.php new file mode 100644 index 0000000..2aaa144 --- /dev/null +++ b/resources/views/components/passkey-registration.blade.php @@ -0,0 +1,116 @@ +@assets +@vite('resources/js/passkeys.js') +@endassets + +
+ + + + + +
diff --git a/resources/views/components/passkey-verify.blade.php b/resources/views/components/passkey-verify.blade.php new file mode 100644 index 0000000..47dd9a8 --- /dev/null +++ b/resources/views/components/passkey-verify.blade.php @@ -0,0 +1,76 @@ +@props([ + 'optionsRoute' => 'passkey.login-options', + 'submitRoute' => 'passkey.login', + 'label' => __('Sign in with a passkey'), + 'loadingLabel' => __('Authenticating...'), + 'separator' => __('Or continue with email'), +]) + +@assets +@vite('resources/js/passkeys.js') +@endassets + +
+ +
diff --git a/resources/views/components/placeholder-pattern.blade.php b/resources/views/components/placeholder-pattern.blade.php new file mode 100644 index 0000000..8a434f0 --- /dev/null +++ b/resources/views/components/placeholder-pattern.blade.php @@ -0,0 +1,12 @@ +@props([ + 'id' => uniqid(), +]) + + + + + + + + + diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php new file mode 100644 index 0000000..8f08c05 --- /dev/null +++ b/resources/views/dashboard.blade.php @@ -0,0 +1,18 @@ + +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
diff --git a/resources/views/flux/icon/book-open-text.blade.php b/resources/views/flux/icon/book-open-text.blade.php new file mode 100644 index 0000000..bff20a3 --- /dev/null +++ b/resources/views/flux/icon/book-open-text.blade.php @@ -0,0 +1,47 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php + if ($variant === 'solid') { + throw new \Exception('The "solid" variant is not supported in Lucide.'); + } + + $classes = Flux::classes('shrink-0')->add( + match ($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }, + ); + + $strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + }; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + + + + diff --git a/resources/views/flux/icon/chevrons-up-down.blade.php b/resources/views/flux/icon/chevrons-up-down.blade.php new file mode 100644 index 0000000..bf1ba2b --- /dev/null +++ b/resources/views/flux/icon/chevrons-up-down.blade.php @@ -0,0 +1,43 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php + if ($variant === 'solid') { + throw new \Exception('The "solid" variant is not supported in Lucide.'); + } + + $classes = Flux::classes('shrink-0')->add( + match ($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }, + ); + + $strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + }; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + diff --git a/resources/views/flux/icon/folder-git-2.blade.php b/resources/views/flux/icon/folder-git-2.blade.php new file mode 100644 index 0000000..292171b --- /dev/null +++ b/resources/views/flux/icon/folder-git-2.blade.php @@ -0,0 +1,45 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php + if ($variant === 'solid') { + throw new \Exception('The "solid" variant is not supported in Lucide.'); + } + + $classes = Flux::classes('shrink-0')->add( + match ($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }, + ); + + $strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + }; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + + diff --git a/resources/views/flux/icon/layout-grid.blade.php b/resources/views/flux/icon/layout-grid.blade.php new file mode 100644 index 0000000..88c5698 --- /dev/null +++ b/resources/views/flux/icon/layout-grid.blade.php @@ -0,0 +1,45 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php + if ($variant === 'solid') { + throw new \Exception('The "solid" variant is not supported in Lucide.'); + } + + $classes = Flux::classes('shrink-0')->add( + match ($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }, + ); + + $strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, + }; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + + diff --git a/resources/views/flux/navlist/group.blade.php b/resources/views/flux/navlist/group.blade.php new file mode 100644 index 0000000..5e691a2 --- /dev/null +++ b/resources/views/flux/navlist/group.blade.php @@ -0,0 +1,51 @@ +@props([ + 'expandable' => false, + 'expanded' => true, + 'heading' => null, +]) + + + +class('group/disclosure') }} + @if ($expanded === true) open @endif + data-flux-navlist-group +> + + + + + + + +
class('block space-y-[2px]') }}> +
+
{{ $heading }}
+
+ +
+ {{ $slot }} +
+
+ + + +
class('block space-y-[2px]') }}> + {{ $slot }} +
+ + diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..037dd1b --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,5 @@ + + + {{ $slot }} + + diff --git a/resources/views/layouts/app/header.blade.php b/resources/views/layouts/app/header.blade.php new file mode 100644 index 0000000..989b8e4 --- /dev/null +++ b/resources/views/layouts/app/header.blade.php @@ -0,0 +1,84 @@ + + + + @include('partials.head') + + + + + + + + + + {{ __('Dashboard') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ __('Dashboard') }} + + + + + + + + + {{ __('Repository') }} + + + {{ __('Documentation') }} + + + + + {{ $slot }} + + @persist('toast') + + + + @endpersist + + @fluxScripts + + diff --git a/resources/views/layouts/app/sidebar.blade.php b/resources/views/layouts/app/sidebar.blade.php new file mode 100644 index 0000000..f8caf95 --- /dev/null +++ b/resources/views/layouts/app/sidebar.blade.php @@ -0,0 +1,120 @@ + + + + @include('partials.head') + + + + + + + + + + + + {{ __('Dashboard') }} + + + {{ __('Certificates') }} + + + {{ __('Categories') }} + + + + + + + + + {{ __('Repository') }} + + + + {{ __('Documentation') }} + + + + + + + + + + + + + + + + + + +
+
+ + +
+ {{ auth()->user()->name }} + {{ auth()->user()->email }} +
+
+
+
+ + + + + + {{ __('Settings') }} + + + + @foreach (\App\Enums\Locale::cases() as $locale) + + {{ $locale->label() }} + + @endforeach + + + + + +
+ @csrf + + {{ __('Log out') }} + +
+
+
+
+ + {{ $slot }} + + @persist('toast') + + + + @endpersist + + @fluxScripts + + diff --git a/resources/views/layouts/auth.blade.php b/resources/views/layouts/auth.blade.php new file mode 100644 index 0000000..7150091 --- /dev/null +++ b/resources/views/layouts/auth.blade.php @@ -0,0 +1,3 @@ + + {{ $slot }} + diff --git a/resources/views/layouts/auth/card.blade.php b/resources/views/layouts/auth/card.blade.php new file mode 100644 index 0000000..c5cf75a --- /dev/null +++ b/resources/views/layouts/auth/card.blade.php @@ -0,0 +1,33 @@ + + + + @include('partials.head') + + + + + @persist('toast') + + + + @endpersist + + @fluxScripts + + diff --git a/resources/views/layouts/auth/simple.blade.php b/resources/views/layouts/auth/simple.blade.php new file mode 100644 index 0000000..dd5866f --- /dev/null +++ b/resources/views/layouts/auth/simple.blade.php @@ -0,0 +1,33 @@ + + + + @include('partials.head') + + +
+ +
+ + + + @persist('toast') + + + + @endpersist + + @fluxScripts + + diff --git a/resources/views/layouts/auth/split.blade.php b/resources/views/layouts/auth/split.blade.php new file mode 100644 index 0000000..a25d1d6 --- /dev/null +++ b/resources/views/layouts/auth/split.blade.php @@ -0,0 +1,50 @@ + + + + @include('partials.head') + + +
+ + +
+ + @persist('toast') + + + + @endpersist + + @fluxScripts + + diff --git a/resources/views/mail/shared-certificate-links.blade.php b/resources/views/mail/shared-certificate-links.blade.php new file mode 100644 index 0000000..52650c7 --- /dev/null +++ b/resources/views/mail/shared-certificate-links.blade.php @@ -0,0 +1,18 @@ + +# {{ __('Certificates shared with you') }} + +{{ __(':name has shared the following certificates with you.', ['name' => $senderName]) }} + +@foreach ($links as $link) + +{{ $link['title'] }} + +@endforeach + +{{ __('These links are valid until :date, after which they will stop working.', ['date' => $expiresAt]) }} + +{{ __('If you did not expect this email, you can safely ignore it.') }} + +{{ __('Thanks') }},
+{{ config('app.name') }} +
diff --git a/resources/views/mail/shared-certificates.blade.php b/resources/views/mail/shared-certificates.blade.php new file mode 100644 index 0000000..d190b17 --- /dev/null +++ b/resources/views/mail/shared-certificates.blade.php @@ -0,0 +1,12 @@ + +# {{ __('Certificates shared with you') }} + +{{ __(':name has shared certificates with you as a password-protected ZIP archive attached to this email.', ['name' => $senderName]) }} + +{{ __('For your security, the password is sent separately through another channel (for example SMS or WhatsApp). You will need it to open the archive.') }} + +{{ __('If you did not expect this email, you can safely ignore it.') }} + +{{ __('Thanks') }},
+{{ config('app.name') }} +
diff --git a/resources/views/marketing/home.blade.php b/resources/views/marketing/home.blade.php new file mode 100644 index 0000000..0f20b8c --- /dev/null +++ b/resources/views/marketing/home.blade.php @@ -0,0 +1,518 @@ +@extends('marketing.layout') + +@section('title', 'Certified — Nooit meer verlopen certificaten. Volledig AVG-proof.') + +@section('content') + +{{-- ============ HERO ============ --}} +
+ + + + +
+
+

+ + + + + Gebouwd voor organisaties die met VOG's en persoonsgegevens werken +

+ +

+ Nooit meer verlopen certificaten. + Volledig AVG-proof. +

+ +

+ Certified scant je certificaten automatisch in, bewaakt elke vervaldatum en deelt documenten alleen versleuteld. Zo blijft je organisatie aantoonbaar compliant — zonder Excel-lijstjes en losse e-mailbijlagen. +

+ + + +

+ Gratis tot 5 certificaten · Geen creditcard nodig · 🇪🇺 Europese hosting +

+
+ + {{-- Dashboard mock-up --}} +
+ + + +
+
+
+ +{{-- ============ SOCIAL PROOF STRIP ============ --}} +
+
+

+ Ontworpen voor sectoren waar certificaten verplicht zijn +

+
+ Kinderopvang + Zorg & welzijn + Bouw & techniek + Transport & logistiek + Detachering +
+
+
+ +{{-- ============ FEATURES ============ --}} +
+
+
+

Features

+

+ Van scan tot reminder tot veilig delen +

+

+ Drie pijlers die samen je volledige certificaatbeheer afdekken — zonder handwerk. +

+
+ + {{-- Pillar A: OCR demo --}} +
+
+ + + Automatische OCR-scan + +

+ Upload een PDF, wij vullen de rest in +

+

+ Sleep een certificaat in Certified en de OCR-engine herkent automatisch de titel, uitgever en vervaldatum. Geen overtypwerk, geen tikfouten in vervaldatums — en jij kiest zelf welke Europese OCR-provider je data verwerkt. +

+
    + @foreach ([ + 'Herkent titel, uitgever en vervaldatum in seconden', + 'Keuze uit Europese providers: Klippa of Mistral AI', + 'Werkt met PDF, JPG en PNG — ook met scans en foto\'s', + ] as $point) +
  • + + {{ $point }} +
  • + @endforeach +
+
+ + {{-- Interactive OCR mock --}} +
+
+ {{-- Mock document being scanned --}} + + + {{-- Extracted fields --}} +
+
+ @foreach ([['title', 'Titel'], ['issuer', 'Uitgever'], ['expires', 'Vervaldatum']] as [$field, $label]) +
+

{{ $label }}

+
+ + +
+
+ @endforeach +
+
+
+ + {{-- Progress + trigger --}} +
+
+
+ + +
+
+
+
+
+ + + +

+ + Certificaat herkend en ingevuld — klaar om op te slaan +

+
+
+
+ + {{-- Pillar B: Reminders timeline --}} +
+
+ + + Slimme reminders + +

+ Op tijd gewaarschuwd, elke keer weer +

+

+ Certified controleert dagelijks alle vervaldatums en stuurt automatisch reminders — ruim op tijd om een certificaat te verlengen. Verloopt er toch iets? Dan krijg je direct een urgente melding, zodat het nooit onopgemerkt blijft. +

+

+ Jij bepaalt zelf hoeveel maanden van tevoren je gewaarschuwd wilt worden. +

+
+ + {{-- Timeline visual --}} +
+ + +
+ {{-- -2 months --}} +
+ + + +
+
+

2 maanden van tevoren

+ Reminder +
+

“BHV Diploma — A. Bakker” verloopt over 2 maanden

+

Rustig de tijd om een herhalingscursus in te plannen.

+
+
+ + {{-- -1 month --}} +
+ + + +
+
+

1 maand van tevoren

+ Herinnering +
+

Nog geen nieuw certificaat geüpload

+

Certified herinnert je opnieuw zolang het niet is verlengd.

+
+
+ + {{-- Expired today --}} +
+ + + +
+
+

Vandaag verlopen

+ Urgent +
+

Direct een urgente melding in je inbox

+

Zo weet je meteen welke medewerker niet meer inzetbaar is.

+
+
+
+
+
+ + {{-- Pillar C: Secure sharing --}} +
+
+ + + Secure sharing + +

+ Stop met certificaten mailen als losse bijlage +

+

+ Een VOG met BSN als onbeveiligde bijlage rondsturen is een datalek dat wacht om te gebeuren. Certified deelt documenten alleen versleuteld en tijdelijk. +

+
+ +
+ + + {{-- The old way --}} +
+
+ + + +

E-mailbijlage (oude manier)

+
+ + {{-- Mock email --}} + + +
    + @foreach ([ + 'Bijlage blijft voor altijd in inboxen en op mailservers staan', + 'Geen controle: doorsturen, kopiëren en lekken kan ongemerkt', + 'BSN onversleuteld verstuurd — een meldplichtig datalek onder de AVG', + ] as $risk) +
  • + + {!! $risk !!} +
  • + @endforeach +
+
+ + {{-- The Certified way --}} +
+
+ + + +

Delen met Certified

+
+ + {{-- Mock share --}} + + +
    + @foreach ([ + 'AES-256 versleutelde ZIP met een apart verstuurd wachtwoord', + 'Tijdelijke ondertekende URL die na 48 uur automatisch verloopt', + 'Volledig AVG-proof: geen kopieën die blijven rondslingeren', + ] as $benefit) +
  • + + {!! $benefit !!} +
  • + @endforeach +
+
+
+
+
+
+ +{{-- ============ TRUST / PRIVACY ============ --}} +
+ + + +
+
+

Europese privacy & security

+

+ Gevoelige gegevens verdienen Europese waarborgen +

+

+ VOG's en BSN-nummers zijn bijzondere persoonsgegevens. Daarom is Certified vanaf de eerste regel code gebouwd op privacy-by-design — en blijft je data binnen de EU. +

+
+ +
+ @foreach ([ + [ + 'icon' => 'M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418', + 'title' => '100% Europese hosting', + 'body' => 'Alle data staat in Europese datacenters, onder Europees recht. Geen Amerikaanse cloud, geen CLOUD Act, volledig AVG/GDPR-compliant.', + ], + [ + 'icon' => 'M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z', + 'title' => 'AES-256 versleuteld delen', + 'body' => 'Gedeelde certificaten worden verpakt in een AES-256 versleutelde ZIP. Links zijn ondertekend en verlopen automatisch na 48 uur.', + ], + [ + 'icon' => 'M7.5 3.75H6A2.25 2.25 0 0 0 3.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0 1 20.25 6v1.5m0 9V18A2.25 2.25 0 0 1 18 20.25h-1.5m-9 0H6A2.25 2.25 0 0 1 3.75 18v-1.5M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z', + 'title' => 'Europese OCR-providers', + 'body' => 'Kies zelf wie je documenten leest: Klippa (Nederland) of Mistral AI (Frankrijk). Je documenten verlaten Europa nooit — jij houdt de regie.', + ], + ] as $card) +
+ + + +

{{ $card['title'] }}

+

{{ $card['body'] }}

+
+ @endforeach +
+ + +
+
+ +{{-- ============ PRICING ============ --}} +
+
+
+

Pricing

+

+ Eerlijke prijzen, geen verrassingen +

+

+ Start gratis en groei mee wanneer je meer certificaten beheert. Elk pakket bevat OCR, reminders en veilig delen. +

+
+ + @include('marketing.partials.pricing-grid') +
+
+ +{{-- ============ FAQ ============ --}} +@include('marketing.partials.faq', ['faqs' => [ + ['q' => 'Is Certified geschikt voor documenten met BSN-nummers, zoals een VOG?', 'a' => 'Ja, daar is Certified juist voor gebouwd. Documenten staan versleuteld op Europese servers, worden nooit als open bijlage gedeeld en zijn alleen toegankelijk voor geautoriseerde gebruikers. Delen gebeurt via een tijdelijke ondertekende link of een AES-256 versleutelde ZIP.'], + ['q' => 'Hoe werkt de automatische OCR-scan?', 'a' => 'Je uploadt een PDF of foto van het certificaat. Onze OCR-engine herkent automatisch de titel, uitgever en vervaldatum en vult die voor je in. Jij controleert en slaat op — dat is alles. Je kiest zelf welke Europese provider (Klippa of Mistral AI) de verwerking doet.'], + ['q' => 'Wanneer krijg ik een reminder als een certificaat bijna verloopt?', 'a' => 'Standaard ontvang je twee maanden van tevoren een e-mail, en daarna opnieuw zolang het certificaat niet verlengd is. Verloopt een certificaat vandaag, dan krijg je direct een urgente melding. Het aantal maanden stel je zelf in per account.'], + ['q' => 'Waar wordt mijn data opgeslagen?', 'a' => 'Uitsluitend in Europese datacenters, onder Europees recht. We gebruiken geen Amerikaanse cloudproviders, waardoor je data niet onder de Amerikaanse CLOUD Act valt. Zo voldoe je aantoonbaar aan de AVG.'], + ['q' => 'Kan ik Certified eerst vrijblijvend proberen?', 'a' => 'Zeker. Het Gratis-pakket is voor altijd gratis en laat je tot 5 certificaten beheren — zonder creditcard. Groei je eruit, dan upgrade je eenvoudig naar Starter (20 certificaten) of Onbeperkt.'], + ['q' => 'Sluiten jullie een verwerkersovereenkomst af?', 'a' => 'Ja. Voor alle betaalde abonnementen bieden we een standaard verwerkersovereenkomst aan, inclusief afspraken over dataretentie en de door jou gekozen OCR-provider.'], +]]) + +{{-- ============ BOTTOM CTA ============ --}} +@include('marketing.partials.cta') + +@endsection diff --git a/resources/views/marketing/layout.blade.php b/resources/views/marketing/layout.blade.php new file mode 100644 index 0000000..cd19083 --- /dev/null +++ b/resources/views/marketing/layout.blade.php @@ -0,0 +1,38 @@ + + + + + + + @yield('title', 'Certified — Nooit meer verlopen certificaten') + + + + + + + @fonts + + {{-- Same storage key as the Flux dashboard, so the visitor's choice carries over after login. --}} + + + @vite(['resources/css/app.css', 'resources/js/marketing.js']) + + + + @include('marketing.partials.nav') + +
+ @yield('content') +
+ + @include('marketing.partials.footer') + + + diff --git a/resources/views/marketing/partials/cta.blade.php b/resources/views/marketing/partials/cta.blade.php new file mode 100644 index 0000000..aed420b --- /dev/null +++ b/resources/views/marketing/partials/cta.blade.php @@ -0,0 +1,26 @@ +
+
+ + + +
+

+ Regel je compliance vandaag nog. Niet pas na de audit. +

+

+ Binnen tien minuten staan je eerste certificaten in Certified — inclusief automatische reminders. Start gratis met maximaal 5 certificaten. +

+ +

Gratis tot 5 certificaten · Geen creditcard nodig · Opzeggen wanneer je wilt

+
+
+
diff --git a/resources/views/marketing/partials/faq.blade.php b/resources/views/marketing/partials/faq.blade.php new file mode 100644 index 0000000..37a2352 --- /dev/null +++ b/resources/views/marketing/partials/faq.blade.php @@ -0,0 +1,36 @@ +{{-- Expects: $faqs (array of ['q' => ..., 'a' => ...]), optional $faqTitle / $faqSubtitle --}} +
+
+

+ {{ $faqTitle ?? 'Veelgestelde vragen' }} +

+

+ {{ $faqSubtitle ?? 'Staat je vraag er niet tussen? Mail ons via support@certified.eu.' }} +

+
+ +
+ @foreach ($faqs as $faq) +
+ +
+

{{ $faq['a'] }}

+
+
+ @endforeach +
+
diff --git a/resources/views/marketing/partials/footer.blade.php b/resources/views/marketing/partials/footer.blade.php new file mode 100644 index 0000000..7a485a6 --- /dev/null +++ b/resources/views/marketing/partials/footer.blade.php @@ -0,0 +1,51 @@ + diff --git a/resources/views/marketing/partials/nav.blade.php b/resources/views/marketing/partials/nav.blade.php new file mode 100644 index 0000000..e574c95 --- /dev/null +++ b/resources/views/marketing/partials/nav.blade.php @@ -0,0 +1,118 @@ +
+ +
diff --git a/resources/views/marketing/partials/pricing-grid.blade.php b/resources/views/marketing/partials/pricing-grid.blade.php new file mode 100644 index 0000000..1063f8e --- /dev/null +++ b/resources/views/marketing/partials/pricing-grid.blade.php @@ -0,0 +1,96 @@ +
+
+ @foreach ([ + [ + 'name' => 'Gratis', + 'description' => 'Voor wie Certified wil proberen of maar een handvol certificaten beheert.', + 'price' => '0', + 'priceNote' => 'Voor altijd gratis, geen creditcard nodig', + 'featured' => false, + 'cta' => 'Start gratis', + 'features' => [ + 'Maximaal 5 certificaten', + 'Automatische OCR-scan', + 'Slimme e-mailreminders', + 'Veilig delen via signed URL', + ], + ], + [ + 'name' => 'Starter', + 'description' => 'Voor zzp\'ers en kleine praktijken met een groeiende certificaatmap.', + 'price' => '12,50', + 'priceNote' => 'Eén vast bedrag, jaarlijks gefactureerd', + 'featured' => true, + 'cta' => 'Start met Starter', + 'features' => [ + 'Maximaal 20 certificaten', + 'Automatische OCR-scan', + 'Slimme e-mailreminders', + 'Veilig delen via signed URL', + 'AES-256 versleutelde ZIP-export', + ], + ], + [ + 'name' => 'Onbeperkt', + 'description' => 'Voor organisaties die al hun certificaten op één plek willen bewaken.', + 'price' => '45', + 'priceNote' => 'Eén vast bedrag, jaarlijks gefactureerd', + 'featured' => false, + 'cta' => 'Start met Onbeperkt', + 'features' => [ + 'Onbeperkt certificaten', + 'Automatische OCR-scan', + 'Slimme e-mailreminders', + 'Veilig delen via signed URL', + 'AES-256 versleutelde ZIP-export', + 'Prioriteit support', + ], + ], + ] as $tier) +
$tier['featured'], + 'border border-slate-200 bg-white shadow-sm hover:-translate-y-1 hover:shadow-xl hover:shadow-slate-900/5 dark:border-slate-800 dark:bg-slate-900 dark:hover:shadow-black/30' => ! $tier['featured'], + ])> + @if ($tier['featured']) + + Meest gekozen + + @endif + +

{{ $tier['name'] }}

+

{{ $tier['description'] }}

+ +
+ €{{ $tier['price'] }} + / jaar +
+

{{ $tier['priceNote'] }}

+ +
    + @foreach ($tier['features'] as $feature) +
  • + + {{ $feature }} +
  • + @endforeach +
+ + $tier['featured'], + 'border border-slate-300 text-slate-700 hover:border-emerald-500 hover:text-emerald-700 dark:border-slate-700 dark:text-slate-200 dark:hover:border-emerald-500 dark:hover:text-emerald-400' => ! $tier['featured'], + ]) + >{{ $tier['cta'] }} +
+ @endforeach +
+ +

+ Alle prijzen zijn per jaar en exclusief btw. Start gratis — upgraden kan altijd, opzeggen ook. +

+
diff --git a/resources/views/marketing/pricing.blade.php b/resources/views/marketing/pricing.blade.php new file mode 100644 index 0000000..47eaf20 --- /dev/null +++ b/resources/views/marketing/pricing.blade.php @@ -0,0 +1,70 @@ +@extends('marketing.layout') + +@section('title', 'Pricing — Certified') +@section('description', 'Eerlijke prijzen voor veilig certificaatbeheer. Start gratis met maximaal 5 certificaten — inclusief OCR, slimme reminders en AVG-proof delen.') + +@section('content') + +{{-- ============ HEADER + GRID ============ --}} +
+ + + +
+
+

Pricing

+

+ Compliance hoeft niet duur te zijn +

+

+ Eén verlopen certificaat kost al snel meer dan een jaar Certified. Start gratis en betaal pas als je meer certificaten beheert. +

+
+ +
+ @include('marketing.partials.pricing-grid') +
+
+
+ +{{-- ============ VALUE REASSURANCE ============ --}} +
+
+

Inbegrepen bij elk pakket

+
+ @foreach ([ + ['title' => 'Europese hosting', 'body' => 'Alle data in EU-datacenters, volledig AVG-proof.'], + ['title' => 'Automatische OCR', 'body' => 'Titel, uitgever en vervaldatum automatisch herkend.'], + ['title' => 'Slimme reminders', 'body' => 'E-mail op het moment dat jij instelt, plus urgente meldingen.'], + ['title' => 'Veilig delen', 'body' => 'Ondertekende links (48u) en AES-256 versleutelde ZIP\'s.'], + ] as $item) +
+ + + +

{{ $item['title'] }}

+

{{ $item['body'] }}

+
+ @endforeach +
+
+
+ +{{-- ============ FAQ ============ --}} +@include('marketing.partials.faq', [ + 'faqTitle' => 'Vragen over prijzen', + 'faqSubtitle' => 'Alles over facturering, opzeggen en overstappen.', + 'faqs' => [ + ['q' => 'Is het Gratis-pakket echt gratis?', 'a' => 'Ja, voor altijd. Je beheert er tot 5 certificaten mee, inclusief OCR-scan, reminders en veilig delen. Geen creditcard, geen verborgen kosten — pas als je meer dan 5 certificaten wilt bewaken, stap je over op een betaald pakket.'], + ['q' => 'Hoe werkt de facturering?', 'a' => 'Betaalde pakketten worden één keer per jaar gefactureerd: €12,50 per jaar voor Starter, €45 per jaar voor Onbeperkt, beide exclusief btw. Je ontvangt een factuur die je direct in je administratie kunt verwerken.'], + ['q' => 'Wat gebeurt er als ik over mijn certificatenlimiet ga?', 'a' => 'Niets ergs — je data blijft gewoon staan. We sturen een melding en je kunt eenvoudig upgraden naar het volgende pakket. We verwijderen nooit certificaten omdat je over een limiet heen bent.'], + ['q' => 'Kan ik tussentijds upgraden, downgraden of opzeggen?', 'a' => 'Upgraden kan op elk moment; je betaalt alleen het verschil voor de rest van het jaar. Downgraden of opzeggen gaat in bij de volgende verlengingsdatum. Na opzegging kun je al je data exporteren; daarna verwijderen we alles definitief binnen 30 dagen.'], + ['q' => 'Zit de verwerkersovereenkomst bij de prijs in?', 'a' => 'Ja, bij de betaalde pakketten leveren we een standaard verwerkersovereenkomst zonder extra kosten.'], + ['q' => 'Zijn er kosten per OCR-scan?', 'a' => 'Nee. OCR-scans zitten bij elk pakket inbegrepen — ook bij Gratis. Je betaalt nooit per scan.'], + ], +]) + +{{-- ============ BOTTOM CTA ============ --}} +@include('marketing.partials.cta') + +@endsection diff --git a/resources/views/marketing/security.blade.php b/resources/views/marketing/security.blade.php new file mode 100644 index 0000000..70289c1 --- /dev/null +++ b/resources/views/marketing/security.blade.php @@ -0,0 +1,101 @@ +@extends('marketing.layout') + +@section('title', 'Security & privacy — Certified') +@section('description', 'Hoe Certified gevoelige certificaten beschermt: 100% Europese hosting, AES-256 versleuteld delen en keuze uit Europese OCR-providers. Volledig AVG/GDPR-compliant.') + +@section('content') + +{{-- ============ HEADER ============ --}} +
+ + + +
+

+ 🇪🇺 Privacy-by-design, gebouwd in Europa +

+

+ Security & privacy als fundament, niet als bijzaak +

+

+ Certified beheert documenten met bijzondere persoonsgegevens — VOG's, diploma's, BSN-nummers. Op deze pagina lees je precies hoe we die gegevens beschermen. +

+
+
+ +{{-- ============ PILLARS IN DETAIL ============ --}} +
+ @foreach ([ + [ + 'kicker' => '01 — Hosting & jurisdictie', + 'title' => '100% Europese hosting, onder Europees recht', + 'body' => 'Alle applicatie- en documentdata staat in Europese datacenters. We gebruiken bewust geen Amerikaanse hyperscalers: je data valt daardoor niet onder de Amerikaanse CLOUD Act en blijft volledig binnen de reikwijdte van de AVG/GDPR. Back-ups worden versleuteld opgeslagen, eveneens binnen de EU.', + 'points' => ['Datacenters in de EU, data verlaat Europa nooit', 'Geen Amerikaanse cloudproviders in de keten', 'Versleutelde back-ups binnen de EU'], + ], + [ + 'kicker' => '02 — Versleuteling', + 'title' => 'Versleuteld in rust, onderweg en bij het delen', + 'body' => 'Documenten worden versleuteld opgeslagen en al het verkeer loopt via TLS. Deel je een certificaat met een externe partij, dan gebeurt dat via een AES-256 versleutelde ZIP met een apart verstuurd wachtwoord, of via een ondertekende link die na 48 uur automatisch onbruikbaar wordt. API-sleutels van OCR-providers slaan we versleuteld op.', + 'points' => ['AES-256 versleutelde ZIP-exports', 'Ondertekende deel-links met een maximale geldigheid van 48 uur', 'TLS voor al het verkeer, versleutelde opslag van API-sleutels'], + ], + [ + 'kicker' => '03 — OCR-verwerking', + 'title' => 'Jij kiest welke Europese partij je documenten leest', + 'body' => 'Voor de automatische tekstherkenning kies je zelf een verwerker: Klippa (Nederland) of Mistral AI (Frankrijk) — beide Europese bedrijven onder Europese jurisdictie. Wil je helemaal geen externe verwerking? Zet OCR dan simpelweg uit en vul velden handmatig in. Jouw data, jouw keuze.', + 'points' => ['Klippa: Nederlandse document-AI specialist', 'Mistral AI: Frans AI-bedrijf, EU-gehost', 'OCR volledig uitschakelbaar per account'], + ], + [ + 'kicker' => '04 — Toegang & isolatie', + 'title' => 'Strikte scheiding tussen organisaties en gebruikers', + 'body' => 'Elke gebruiker ziet uitsluitend de eigen certificaten — afgedwongen op databaseniveau, niet alleen in de interface. Inloggen kan met wachtwoord of passkeys, en gedeelde documenten zijn alleen bereikbaar via cryptografisch ondertekende URL\'s die niet te raden of te vervalsen zijn.', + 'points' => ['Data-isolatie afgedwongen op databaseniveau', 'Ondersteuning voor passkeys (phishing-bestendig inloggen)', 'Cryptografisch ondertekende deel-URL\'s'], + ], + ] as $block) +
+

{{ $block['kicker'] }}

+

{{ $block['title'] }}

+

{{ $block['body'] }}

+
    + @foreach ($block['points'] as $point) +
  • + + {{ $point }} +
  • + @endforeach +
+
+ @endforeach +
+ +{{-- ============ AVG BANNER ============ --}} +
+
+ + + +

+ Verwerkersovereenkomst inbegrepen +

+

+ Als jouw organisatie certificaten van medewerkers beheert, ben jij verwerkingsverantwoordelijke en zijn wij verwerker. Daarom leveren we bij elk betaald abonnement een heldere verwerkersovereenkomst — zodat je richting de Autoriteit Persoonsgegevens direct kunt aantonen dat het geregeld is. +

+
+
+ +{{-- ============ FAQ ============ --}} +@include('marketing.partials.faq', [ + 'faqTitle' => 'Vragen over security', + 'faqSubtitle' => 'De vragen die privacy officers en functionarissen gegevensbescherming ons het vaakst stellen.', + 'faqs' => [ + ['q' => 'Valt mijn data onder de Amerikaanse CLOUD Act?', 'a' => 'Nee. Certified draait uitsluitend op Europese infrastructuur van Europese leveranciers. Er is geen Amerikaanse partij in de hostingketen, dus de CLOUD Act is niet van toepassing op jouw data.'], + ['q' => 'Wat gebeurt er met een document tijdens de OCR-scan?', 'a' => 'Het document wordt via een beveiligde verbinding naar de door jou gekozen Europese provider (Klippa of Mistral AI) gestuurd, die er tekst uit herkent en het resultaat teruggeeft. Wil je dit niet, dan zet je OCR uit en vul je de velden handmatig in.'], + ['q' => 'Hoe lang blijft een gedeelde link geldig?', 'a' => 'Maximaal 48 uur. Daarna is de ondertekende URL cryptografisch ongeldig en kan niemand het document meer openen — ook niet met de oude link. Versleutelde ZIP-exports blijven beveiligd met AES-256 en het apart verstuurde wachtwoord.'], + ['q' => 'Wie kan er bij de certificaten van mijn organisatie?', 'a' => 'Alleen de gebruikers van jouw eigen account. De scheiding tussen gebruikers wordt op databaseniveau afgedwongen. Onze eigen medewerkers hebben geen toegang tot documentinhoud, behalve wanneer jij daar bij een supportverzoek expliciet toestemming voor geeft.'], + ['q' => 'Kan ik al mijn data exporteren of laten verwijderen?', 'a' => 'Ja. Je kunt op elk moment al je certificaten en metadata exporteren. Zeg je op, dan verwijderen we je data — inclusief back-ups — binnen 30 dagen definitief, conform de AVG.'], + ], +]) + +{{-- ============ BOTTOM CTA ============ --}} +@include('marketing.partials.cta') + +@endsection diff --git a/resources/views/pages/auth/confirm-password.blade.php b/resources/views/pages/auth/confirm-password.blade.php new file mode 100644 index 0000000..dc8748a --- /dev/null +++ b/resources/views/pages/auth/confirm-password.blade.php @@ -0,0 +1,36 @@ + +
+ + + + + + +
+ @csrf + + + + + {{ __('Confirm') }} + + +
+
diff --git a/resources/views/pages/auth/forgot-password.blade.php b/resources/views/pages/auth/forgot-password.blade.php new file mode 100644 index 0000000..5f1cdad --- /dev/null +++ b/resources/views/pages/auth/forgot-password.blade.php @@ -0,0 +1,31 @@ + +
+ + + + + +
+ @csrf + + + + + + {{ __('Email password reset link') }} + + + +
+ {{ __('Or, return to') }} + {{ __('log in') }} +
+
+
diff --git a/resources/views/pages/auth/login.blade.php b/resources/views/pages/auth/login.blade.php new file mode 100644 index 0000000..9cb91a6 --- /dev/null +++ b/resources/views/pages/auth/login.blade.php @@ -0,0 +1,59 @@ + +
+ + + + + + + +
+ @csrf + + + + + +
+ + + @if (Route::has('password.request')) + + {{ __('Forgot your password?') }} + + @endif +
+ + + + +
+ + {{ __('Log in') }} + +
+ + +
+ {{ __('Don\'t have an account?') }} + {{ __('Sign up') }} +
+
+
diff --git a/resources/views/pages/auth/register.blade.php b/resources/views/pages/auth/register.blade.php new file mode 100644 index 0000000..30ddb89 --- /dev/null +++ b/resources/views/pages/auth/register.blade.php @@ -0,0 +1,69 @@ + +
+ + + + + +
+ @csrf + + + + + + + + + + + + +
+ + {{ __('Create account') }} + +
+ + +
+ {{ __('Already have an account?') }} + {{ __('Log in') }} +
+
+
diff --git a/resources/views/pages/auth/reset-password.blade.php b/resources/views/pages/auth/reset-password.blade.php new file mode 100644 index 0000000..86001bd --- /dev/null +++ b/resources/views/pages/auth/reset-password.blade.php @@ -0,0 +1,54 @@ + +
+ + + + + +
+ @csrf + + + + + + + + + + + + +
+ + {{ __('Reset password') }} + +
+ +
+
diff --git a/resources/views/pages/auth/two-factor-challenge.blade.php b/resources/views/pages/auth/two-factor-challenge.blade.php new file mode 100644 index 0000000..febe7d5 --- /dev/null +++ b/resources/views/pages/auth/two-factor-challenge.blade.php @@ -0,0 +1,101 @@ + +
+
+
+ +
+ +
+ +
+ +
+ @csrf + +
+
+
+ +
+
+ +
+
+ +
+ + @error('recovery_code') + + {{ $message }} + + @enderror +
+ + + {{ __('Continue') }} + +
+ +
+ {{ __('or you can') }} +
+ {{ __('login using a recovery code') }} + {{ __('login using an authentication code') }} +
+
+
+
+
+
diff --git a/resources/views/pages/auth/verify-email.blade.php b/resources/views/pages/auth/verify-email.blade.php new file mode 100644 index 0000000..50e7b44 --- /dev/null +++ b/resources/views/pages/auth/verify-email.blade.php @@ -0,0 +1,29 @@ + +
+ + {{ __('Please verify your email address by clicking on the link we just emailed to you.') }} + + + @if (session('status') == 'verification-link-sent') + + {{ __('A new verification link has been sent to the email address you provided during registration.') }} + + @endif + +
+
+ @csrf + + {{ __('Resend verification email') }} + +
+ +
+ @csrf + + {{ __('Log out') }} + +
+
+
+
diff --git a/resources/views/pages/categories/⚡index.blade.php b/resources/views/pages/categories/⚡index.blade.php new file mode 100644 index 0000000..a09444c --- /dev/null +++ b/resources/views/pages/categories/⚡index.blade.php @@ -0,0 +1,119 @@ +validate([ + 'name' => [ + 'required', + 'string', + 'max:255', + Rule::unique('categories', 'name')->where('user_id', Auth::id()), + ], + ]); + + Category::create(['name' => $this->name]); + + $this->reset('name'); + + Flux::toast(variant: 'success', text: __('Category created.')); + } + + public function delete(int $categoryId): void + { + Category::findOrFail($categoryId)->delete(); + + Flux::modals()->close(); + + Flux::toast(variant: 'success', text: __('Category deleted. Its certificates are now uncategorized.')); + } + + /** + * @return Collection + */ + #[Computed] + public function categories(): Collection + { + return Category::withCount('certificates')->orderBy('name')->get(); + } +}; ?> + +
+
+ {{ __('Categories') }} + {{ __('Organize your certificates with personal categories.') }} +
+ +
+
+ +
+ {{ __('Add') }} +
+ + @if ($this->categories->isEmpty()) + + + {{ __('No categories yet') }} + {{ __('Create your first category to organize your certificates.') }} + + @else + + + {{ __('Name') }} + {{ __('Certificates') }} + + + + + @foreach ($this->categories as $category) + + + {{ $category->name }} + + + {{ $category->certificates_count }} + + + + + {{ __('Delete') }} + + + + +
+
+ {{ __('Delete category?') }} + + {{ __('":name" will be deleted. Certificates in this category are kept and become uncategorized.', ['name' => $category->name]) }} + +
+
+ + {{ __('Cancel') }} + + + {{ __('Delete') }} + +
+
+
+
+
+ @endforeach +
+
+ @endif +
diff --git a/resources/views/pages/certificates/⚡create.blade.php b/resources/views/pages/certificates/⚡create.blade.php new file mode 100644 index 0000000..b0d4afc --- /dev/null +++ b/resources/views/pages/certificates/⚡create.blade.php @@ -0,0 +1,197 @@ +validate([ + 'file' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:10240'], + ]); + + $user = Auth::user(); + + if ($user->ocr_provider === 'none') { + return; + } + + $this->scanning = true; + + $suggestions = app(OcrManager::class) + ->driver($user->ocr_provider) + ->analyze($this->file->getRealPath(), $user->ocr_api_key); + + if (blank($this->title) && filled($suggestions['title'] ?? null)) { + $this->title = $suggestions['title']; + } + + if (blank($this->expires_at) && filled($suggestions['expires_at'] ?? null)) { + $this->expires_at = $suggestions['expires_at']; + } + + $this->scanning = false; + } + + public function save(): void + { + $validated = $this->validate([ + 'file' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:10240'], + 'title' => ['required', 'string', 'max:255'], + 'category_id' => ['nullable', 'integer', 'exists:categories,id'], + 'issuer' => ['nullable', 'string', 'max:255'], + 'certificate_number' => ['nullable', 'string', 'max:255'], + 'issued_at' => ['nullable', 'date', 'before_or_equal:expires_at'], + 'expires_at' => ['required', 'date'], + 'notes' => ['nullable', 'string', 'max:2000'], + ]); + + $path = $this->file->storeAs( + 'certificates/'.Auth::id(), + Str::uuid().'.'.$this->file->getClientOriginalExtension(), + 'local' + ); + + Auth::user()->certificates()->create([ + 'title' => $validated['title'], + 'category_id' => $validated['category_id'] ?: null, + 'issuer' => $validated['issuer'] ?: null, + 'certificate_number' => $validated['certificate_number'] ?: null, + 'issued_at' => $validated['issued_at'] ?: null, + 'file_path' => $path, + 'expires_at' => $validated['expires_at'], + 'notes' => $validated['notes'] ?: null, + ]); + + Flux::toast(variant: 'success', text: __('Certificate added.')); + + $this->redirectRoute('certificates.index', navigate: true); + } + + /** + * @return Collection + */ + #[Computed] + public function categories(): Collection + { + return Category::orderBy('name')->get(); + } +}; ?> + +
+
+ {{ __('Add certificate') }} + {{ __('Upload a document to preview it, then confirm the details — pre-filled automatically if scanning is enabled.') }} +
+ +
+ {{-- Left: live document preview (client-side, before upload completes) --}} +
+
+
+ + {{ __('Select a document to preview it here.') }} +
+ + + + + +
+ + {{ __('Uploading…') }} +
+
+
+ + {{-- Right: form fields --}} +
+ + + @if ($scanning) + + {{ __('Scanning document for suggestions…') }} + + @endif + + + + + {{ __('Uncategorized') }} + @foreach ($this->categories as $category) + {{ $category->name }} + @endforeach + + +
+ + + + +
+ + + +
+ {{ __('Save certificate') }} + {{ __('Cancel') }} +
+ +
+
diff --git a/resources/views/pages/certificates/⚡edit.blade.php b/resources/views/pages/certificates/⚡edit.blade.php new file mode 100644 index 0000000..b5088f5 --- /dev/null +++ b/resources/views/pages/certificates/⚡edit.blade.php @@ -0,0 +1,142 @@ +certificate = $certificate; + $this->title = $certificate->title; + $this->category_id = (string) ($certificate->category_id ?? ''); + $this->issuer = (string) ($certificate->issuer ?? ''); + $this->certificate_number = (string) ($certificate->certificate_number ?? ''); + $this->issued_at = $certificate->issued_at?->toDateString() ?? ''; + $this->expires_at = $certificate->expires_at->toDateString(); + $this->notes = (string) ($certificate->notes ?? ''); + } + + public function save(): void + { + $validated = $this->validate([ + 'title' => ['required', 'string', 'max:255'], + 'category_id' => ['nullable', 'integer', 'exists:categories,id'], + 'issuer' => ['nullable', 'string', 'max:255'], + 'certificate_number' => ['nullable', 'string', 'max:255'], + 'issued_at' => ['nullable', 'date', 'before_or_equal:expires_at'], + 'expires_at' => ['required', 'date'], + 'notes' => ['nullable', 'string', 'max:2000'], + 'file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:10240'], + ]); + + if ($this->file !== null) { + Storage::disk('local')->delete($this->certificate->file_path); + + $this->certificate->file_path = $this->file->storeAs( + 'certificates/'.Auth::id(), + Str::uuid().'.'.$this->file->getClientOriginalExtension(), + 'local' + ); + } + + $this->certificate->fill([ + 'title' => $validated['title'], + 'category_id' => $validated['category_id'] ?: null, + 'issuer' => $validated['issuer'] ?: null, + 'certificate_number' => $validated['certificate_number'] ?: null, + 'issued_at' => $validated['issued_at'] ?: null, + 'expires_at' => $validated['expires_at'], + 'notes' => $validated['notes'] ?: null, + ])->save(); + + Flux::toast(variant: 'success', text: __('Certificate updated.')); + + $this->redirectRoute('certificates.index', navigate: true); + } + + /** + * @return Collection + */ + #[Computed] + public function categories(): Collection + { + return Category::orderBy('name')->get(); + } +}; ?> + +
+
+ {{ __('Edit certificate') }} + {{ __('Update the details or replace the stored document.') }} +
+ +
+ + + + {{ __('Uncategorized') }} + @foreach ($this->categories as $category) + {{ $category->name }} + @endforeach + + +
+ + + + +
+ + + + + +
+ {{ __('Save changes') }} + {{ __('Download current file') }} + {{ __('Cancel') }} +
+ +
diff --git a/resources/views/pages/certificates/⚡index.blade.php b/resources/views/pages/certificates/⚡index.blade.php new file mode 100644 index 0000000..4e963bc --- /dev/null +++ b/resources/views/pages/certificates/⚡index.blade.php @@ -0,0 +1,352 @@ + */ + public array $selected = []; + + public string $shareEmail = ''; + + public string $shareMethod = 'link'; + + public ?string $sharePassword = null; + + public function updatedCategoryFilter(): void + { + $this->resetPage(); + } + + public function sort(string $column): void + { + if (! in_array($column, self::SORTABLE_COLUMNS, true)) { + return; + } + + if ($this->sortBy === $column) { + $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + $this->sortBy = $column; + $this->sortDirection = 'asc'; + } + + $this->resetPage(); + } + + public function delete(int $certificateId): void + { + $certificate = Certificate::findOrFail($certificateId); + + if (filled($certificate->file_path)) { + Storage::disk('local')->delete($certificate->file_path); + } + + $certificate->delete(); + + Flux::modals()->close(); + Flux::toast(variant: 'success', text: __('Certificate deleted.')); + } + + public function share(): void + { + $validated = $this->validate([ + 'shareEmail' => ['required', 'email'], + 'shareMethod' => ['required', Rule::in(['link', 'zip'])], + 'selected' => ['required', 'array', 'min:1'], + 'selected.*' => ['integer'], + ]); + + // Throttle to protect the mail server from being used as a relay. + $key = 'share-certificates:'.Auth::id(); + + if (RateLimiter::tooManyAttempts($key, maxAttempts: 10)) { + $this->addError('shareEmail', __('Too many share attempts. Please try again later.')); + + return; + } + + RateLimiter::hit($key, decaySeconds: 3600); + + $certificates = Certificate::whereIn('id', $validated['selected']) + ->whereNotNull('file_path') + ->get(); + + if ($certificates->isEmpty()) { + $this->addError('selected', __('The selected certificates have no files to share.')); + + return; + } + + $validated['shareMethod'] === 'zip' + ? $this->shareAsZip($certificates, $validated['shareEmail']) + : $this->shareAsLinks($certificates, $validated['shareEmail']); + } + + /** + * @param Collection $certificates + */ + private function shareAsLinks(Collection $certificates, string $email): void + { + $expiresAt = now()->addHours(48); + + $links = $certificates->map(fn (Certificate $certificate) => [ + 'title' => $certificate->title, + 'url' => UrlGenerator::temporarySignedRoute('shared.certificate', $expiresAt, ['certificate' => $certificate->id]), + ])->all(); + + Mail::to($email)->send(new SharedCertificateLinksMail( + senderName: Auth::user()->name, + links: $links, + expiresAt: $expiresAt->toDayDateTimeString(), + )); + + Flux::modals()->close(); + Flux::toast(variant: 'success', text: __('Secure links sent to :email.', ['email' => $email])); + $this->reset('selected', 'shareEmail'); + } + + /** + * @param Collection $certificates + */ + private function shareAsZip(Collection $certificates, string $email): void + { + $password = Str::password(12, symbols: false); + $zipPath = app(CertificateZipper::class)->create($certificates, $password); + + try { + Mail::to($email)->send(new SharedCertificatesMail( + senderName: Auth::user()->name, + zipPath: $zipPath, + )); + } finally { + @unlink($zipPath); + } + + // Show the password so the user can share it via a separate channel. + $this->sharePassword = $password; + $this->reset('selected', 'shareEmail'); + Flux::modal('share')->close(); + Flux::modal('share-password')->show(); + } + + /** + * @return LengthAwarePaginator + */ + #[Computed] + public function certificates(): LengthAwarePaginator + { + $sortBy = in_array($this->sortBy, self::SORTABLE_COLUMNS, true) ? $this->sortBy : 'expires_at'; + $sortDirection = $this->sortDirection === 'desc' ? 'desc' : 'asc'; + + return Certificate::query() + ->with('category') + ->when($this->categoryFilter !== '', fn ($query) => $query->where('category_id', $this->categoryFilter)) + ->orderBy($sortBy, $sortDirection) + ->paginate(10); + } + + /** + * @return Collection + */ + #[Computed] + public function categories(): Collection + { + return Category::orderBy('name')->get(); + } +}; ?> + +
+
+
+ {{ __('Certificates') }} + {{ __('An overview of your certificates and their expiry status.') }} +
+ +
+ + {{ __('All categories') }} + @foreach ($this->categories as $category) + {{ $category->name }} + @endforeach + + + + {{ __('Add certificate') }} + +
+
+ + @if (count($selected) > 0) + + {{ trans_choice(':count certificate selected|:count certificates selected', count($selected), ['count' => count($selected)]) }} + + {{ __('Share') }} + + + @endif + + @if ($this->certificates->isEmpty()) + + + {{ __('No certificates found') }} + + {{ $categoryFilter !== '' + ? __('No certificates in this category. Try another filter.') + : __('Add your first certificate to get started.') }} + + + @else + + + + {{ __('Title') }} + {{ __('Category') }} + {{ __('Expires') }} + {{ __('Status') }} + + + + + @foreach ($this->certificates as $certificate) + @php($status = $certificate->expiryStatus(auth()->user()->reminder_months_before)) + + + + + + {{ $certificate->title }} + + {{ $certificate->category?->name ?? '—' }} + {{ $certificate->expires_at->format('M j, Y') }} + + {{ $status->label() }} + + + + + + + {{ __('Download') }} + + + {{ __('Edit') }} + + + {{ __('Delete') }} + + + + + +
+
+ {{ __('Delete certificate?') }} + + {{ __('":title" and its stored file will be permanently deleted.', ['title' => $certificate->title]) }} + +
+
+ + {{ __('Cancel') }} + + + {{ __('Delete') }} + +
+
+
+
+
+ @endforeach +
+
+ @endif + + {{-- Share modal --}} + +
+
+ {{ __('Share certificates') }} + {{ __('Send the selected certificates to an external recipient.') }} +
+ + + + + + + + +
+ + {{ __('Cancel') }} + + {{ __('Send') }} +
+ +
+ + {{-- ZIP password reveal modal --}} + +
+
+ {{ __('Archive password') }} + + {{ __('The ZIP was emailed. Share this password with the recipient through a different channel (SMS or WhatsApp). It will not be shown again.') }} + +
+ + @if ($sharePassword) + + @endif + +
+ + {{ __('Done') }} + +
+
+
+
diff --git a/resources/views/pages/settings/layout.blade.php b/resources/views/pages/settings/layout.blade.php new file mode 100644 index 0000000..33ac7d8 --- /dev/null +++ b/resources/views/pages/settings/layout.blade.php @@ -0,0 +1,21 @@ +
+
+ + {{ __('Profile') }} + {{ __('Security') }} + {{ __('Appearance') }} + {{ __('Certificates') }} + +
+ + + +
+ {{ $heading ?? '' }} + {{ $subheading ?? '' }} + +
+ {{ $slot }} +
+
+
diff --git a/resources/views/pages/settings/two-factor/⚡recovery-codes.blade.php b/resources/views/pages/settings/two-factor/⚡recovery-codes.blade.php new file mode 100644 index 0000000..21aa4dc --- /dev/null +++ b/resources/views/pages/settings/two-factor/⚡recovery-codes.blade.php @@ -0,0 +1,136 @@ +loadRecoveryCodes(); + } + + /** + * Generate new recovery codes for the user. + */ + public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generateNewRecoveryCodes): void + { + $generateNewRecoveryCodes(auth()->user()); + + $this->loadRecoveryCodes(); + } + + /** + * Load the recovery codes for the user. + */ + private function loadRecoveryCodes(): void + { + $user = auth()->user(); + + if ($user->hasEnabledTwoFactorAuthentication() && $user->two_factor_recovery_codes) { + try { + $this->recoveryCodes = json_decode(decrypt($user->two_factor_recovery_codes), true); + } catch (Exception) { + $this->addError('recoveryCodes', 'Failed to load recovery codes'); + + $this->recoveryCodes = []; + } + } + } +}; ?> + +
+
+
+ + {{ __('2FA recovery codes') }} +
+ + {{ __('Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.') }} + +
+ +
+
+ + + + {{ __('Hide recovery codes') }} + + + @if (filled($recoveryCodes)) + + {{ __('Regenerate codes') }} + + @endif +
+ +
+
+ @error('recoveryCodes') + + @enderror + + @if (filled($recoveryCodes)) +
+ @foreach($recoveryCodes as $code) +
+ {{ $code }} +
+ @endforeach +
+ + {{ __('Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate codes above.') }} + + @endif +
+
+
+
diff --git a/resources/views/pages/settings/⚡appearance.blade.php b/resources/views/pages/settings/⚡appearance.blade.php new file mode 100644 index 0000000..3bfadec --- /dev/null +++ b/resources/views/pages/settings/⚡appearance.blade.php @@ -0,0 +1,47 @@ + + */ + public function locales(): array + { + return Locale::cases(); + } +}; ?> + +
+ @include('partials.settings-heading') + + {{ __('Appearance settings') }} + + + + {{ __('Light') }} + {{ __('Dark') }} + {{ __('System') }} + + + + + {{ __('Language') }} + {{ __('Choose the language used throughout the application') }} + +
+ @foreach ($this->locales() as $locale) + + {{ $locale->label() }} + + @endforeach +
+
+
diff --git a/resources/views/pages/settings/⚡certificates.blade.php b/resources/views/pages/settings/⚡certificates.blade.php new file mode 100644 index 0000000..f1cefa5 --- /dev/null +++ b/resources/views/pages/settings/⚡certificates.blade.php @@ -0,0 +1,91 @@ +reminder_months_before = $user->reminder_months_before; + $this->ocr_provider = $user->ocr_provider; + $this->hasApiKey = filled($user->ocr_api_key); + } + + public function updateSettings(): void + { + $validated = $this->validate([ + 'reminder_months_before' => ['required', 'integer', 'min:0', 'max:24'], + 'ocr_provider' => ['required', Rule::in(OcrManager::availableProviders())], + 'ocr_api_key' => ['nullable', 'string', 'max:255'], + ]); + + $user = Auth::user(); + $user->reminder_months_before = $validated['reminder_months_before']; + $user->ocr_provider = $validated['ocr_provider']; + + // Only overwrite the stored key when a new one is entered, so the + // masked field can be left blank to keep the existing key. + if (filled($validated['ocr_api_key'])) { + $user->ocr_api_key = $validated['ocr_api_key']; + } elseif ($validated['ocr_provider'] === 'none') { + $user->ocr_api_key = null; + } + + $user->save(); + + $this->reset('ocr_api_key'); + $this->hasApiKey = filled($user->ocr_api_key); + + Flux::toast(variant: 'success', text: __('Certificate settings updated.')); + } +}; ?> + +
+ @include('partials.settings-heading') + + +
+ + + + {{ __('None (manual entry)') }} + {{ __('Klippa (EU / NL)') }} + {{ __('Mistral Pixtral (EU / FR)') }} + + + @if ($ocr_provider !== 'none') + + @endif + +
+ {{ __('Save') }} +
+ +
+
diff --git a/resources/views/pages/settings/⚡delete-user-form.blade.php b/resources/views/pages/settings/⚡delete-user-form.blade.php new file mode 100644 index 0000000..93550f8 --- /dev/null +++ b/resources/views/pages/settings/⚡delete-user-form.blade.php @@ -0,0 +1,20 @@ + + +
+
+ {{ __('Delete account') }} + {{ __('Delete your account and all of its resources') }} +
+ + + + {{ __('Delete account') }} + + + + +
diff --git a/resources/views/pages/settings/⚡delete-user-modal.blade.php b/resources/views/pages/settings/⚡delete-user-modal.blade.php new file mode 100644 index 0000000..36075fe --- /dev/null +++ b/resources/views/pages/settings/⚡delete-user-modal.blade.php @@ -0,0 +1,50 @@ +validate([ + 'password' => $this->currentPasswordRules(), + ]); + + tap(Auth::user(), $logout(...))->delete(); + + $this->redirect('/', navigate: true); + } +}; ?> + + +
+
+ {{ __('Are you sure you want to delete your account?') }} + + + {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} + +
+ + + +
+ + {{ __('Cancel') }} + + + + {{ __('Delete account') }} + +
+ +
diff --git a/resources/views/pages/settings/⚡profile.blade.php b/resources/views/pages/settings/⚡profile.blade.php new file mode 100644 index 0000000..452dc02 --- /dev/null +++ b/resources/views/pages/settings/⚡profile.blade.php @@ -0,0 +1,124 @@ +name = Auth::user()->name; + $this->email = Auth::user()->email; + } + + /** + * Update the profile information for the currently authenticated user. + */ + public function updateProfileInformation(): void + { + $user = Auth::user(); + + $validated = $this->validate($this->profileRules($user->id)); + + $user->fill($validated); + + if ($user->isDirty('email')) { + $user->email_verified_at = null; + } + + $user->save(); + + Flux::toast(variant: 'success', text: __('Profile updated.')); + } + + /** + * Send an email verification notification to the current user. + */ + public function resendVerificationNotification(): void + { + $user = Auth::user(); + + if ($user->hasVerifiedEmail()) { + $this->redirectIntended(default: route('dashboard', absolute: false)); + + return; + } + + $user->sendEmailVerificationNotification(); + + Session::flash('status', 'verification-link-sent'); + } + + #[Computed] + public function hasUnverifiedEmail(): bool + { + return Auth::user() instanceof MustVerifyEmail && ! Auth::user()->hasVerifiedEmail(); + } + + #[Computed] + public function showDeleteUser(): bool + { + return ! Auth::user() instanceof MustVerifyEmail + || (Auth::user() instanceof MustVerifyEmail && Auth::user()->hasVerifiedEmail()); + } +}; ?> + +
+ @include('partials.settings-heading') + + {{ __('Profile settings') }} + + +
+ + +
+ + + @if ($this->hasUnverifiedEmail) +
+ + {{ __('Your email address is unverified.') }} + + + {{ __('Click here to re-send the verification email.') }} + + + + @if (session('status') === 'verification-link-sent') + + {{ __('A new verification link has been sent to your email address.') }} + + @endif +
+ @endif +
+ +
+
+ + {{ __('Save') }} + +
+ +
+ + + @if ($this->showDeleteUser) + + @endif +
+
diff --git a/resources/views/pages/settings/⚡security.blade.php b/resources/views/pages/settings/⚡security.blade.php new file mode 100644 index 0000000..b73e639 --- /dev/null +++ b/resources/views/pages/settings/⚡security.blade.php @@ -0,0 +1,341 @@ +canManageTwoFactor = Features::canManageTwoFactorAuthentication(); + + if ($this->canManageTwoFactor) { + if (Fortify::confirmsTwoFactorAuthentication() && is_null(auth()->user()->two_factor_confirmed_at)) { + $disableTwoFactorAuthentication(auth()->user()); + } + + $this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication(); + $this->requiresConfirmation = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'); + } + + $this->canManagePasskeys = Features::canManagePasskeys(); + + if ($this->canManagePasskeys) { + $this->loadPasskeys(); + } + } + + /** + * Update the password for the currently authenticated user. + */ + public function updatePassword(): void + { + try { + $validated = $this->validate([ + 'current_password' => $this->currentPasswordRules(), + 'password' => $this->passwordRules(), + ]); + } catch (ValidationException $e) { + $this->reset('current_password', 'password', 'password_confirmation'); + + throw $e; + } + + Auth::user()->update([ + 'password' => $validated['password'], + ]); + + $this->reset('current_password', 'password', 'password_confirmation'); + + Flux::toast(variant: 'success', text: __('Password updated.')); + } + + /** + * Load the user's passkeys. + */ + public function loadPasskeys(): void + { + $this->passkeys = auth()->user()->passkeys() + ->select(['id', 'name', 'credential', 'created_at', 'last_used_at']) + ->latest() + ->get() + ->map(fn ($passkey) => [ + 'id' => $passkey->id, + 'name' => $passkey->name, + 'authenticator' => $passkey->authenticator, + 'created_at_diff' => $passkey->created_at->diffForHumans(), + 'last_used_at_diff' => $passkey->last_used_at?->diffForHumans(), + ]) + ->toArray(); + } + + /** + * Show the delete confirmation modal. + */ + public function confirmDelete(int $passkeyId): void + { + $passkey = auth()->user()->passkeys()->findOrFail($passkeyId); + + $this->deletingPasskeyId = $passkey->id; + $this->deletingPasskeyName = $passkey->name; + $this->showDeleteModal = true; + } + + /** + * Delete the passkey. + */ + public function deletePasskey(DeletePasskey $deletePasskey): void + { + if (! $this->deletingPasskeyId) { + return; + } + + $passkey = auth()->user()->passkeys()->findOrFail($this->deletingPasskeyId); + + $deletePasskey(auth()->user(), $passkey); + + $this->closeDeleteModal(); + $this->loadPasskeys(); + } + + /** + * Close the delete confirmation modal. + */ + public function closeDeleteModal(): void + { + $this->showDeleteModal = false; + $this->deletingPasskeyId = null; + $this->deletingPasskeyName = ''; + } + + /** + * Handle the two-factor authentication enabled event. + */ + #[On('two-factor-enabled')] + public function onTwoFactorEnabled(): void + { + $this->twoFactorEnabled = true; + } + + /** + * Disable two-factor authentication for the user. + */ + public function disable(DisableTwoFactorAuthentication $disableTwoFactorAuthentication): void + { + $disableTwoFactorAuthentication(auth()->user()); + + $this->twoFactorEnabled = false; + } +}; ?> + +
+ @include('partials.settings-heading') + + {{ __('Security settings') }} + + +
+ + + + +
+ + {{ __('Save') }} + +
+ + + @if ($canManageTwoFactor) +
+ {{ __('Two-factor authentication') }} + {{ __('Manage your two-factor authentication settings') }} + +
+ @if ($twoFactorEnabled) +
+ + {{ __('You will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.') }} + + +
+ + {{ __('Disable 2FA') }} + +
+ + +
+ @else +
+ + {{ __('When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.') }} + + + + + {{ __('Enable 2FA') }} + + + + +
+ @endif +
+
+ @endif + + @if ($canManagePasskeys) +
+ {{ __('Passkeys') }} + {{ __('Manage your passkeys for passwordless sign-in') }} + +
+
+ @forelse ($passkeys as $passkey) +
+
+
+ +
+
+
+

{{ $passkey['name'] }}

+ @if ($passkey['authenticator']) + {{ $passkey['authenticator'] }} + @endif +
+

+ {{ __('Added :time', ['time' => $passkey['created_at_diff']]) }} + @if ($passkey['last_used_at_diff']) + / + {{ __('Last used :time', ['time' => $passkey['last_used_at_diff']]) }} + @endif +

+
+
+ + +
+ @empty +
+
+ +
+

{{ __('No passkeys yet') }}

+ {{ __('Add a passkey to sign in without a password') }} +
+ @endforelse +
+ + +
+
+ @endif +
+ + +
+
+ {{ __('Remove passkey') }} + + {{ __('Are you sure you want to remove the passkey ":name"? You will no longer be able to use it to sign in.', ['name' => $deletingPasskeyName]) }} + +
+ +
+ + {{ __('Cancel') }} + + + {{ __('Remove passkey') }} + +
+
+
+
diff --git a/resources/views/pages/settings/⚡two-factor-setup-modal.blade.php b/resources/views/pages/settings/⚡two-factor-setup-modal.blade.php new file mode 100644 index 0000000..a4fae1a --- /dev/null +++ b/resources/views/pages/settings/⚡two-factor-setup-modal.blade.php @@ -0,0 +1,310 @@ +requiresConfirmation = $requiresConfirmation; + } + + #[On('start-two-factor-setup')] + public function startTwoFactorSetup(): void + { + $enableTwoFactorAuthentication = app(EnableTwoFactorAuthentication::class); + $enableTwoFactorAuthentication(auth()->user()); + + $this->loadSetupData(); + } + + /** + * Load the two-factor authentication setup data for the user. + */ + private function loadSetupData(): void + { + $user = auth()->user()?->fresh(); + + try { + if (! $user || ! $user->two_factor_secret) { + throw new Exception('Two-factor setup secret is not available.'); + } + + $this->qrCodeSvg = $user->twoFactorQrCodeSvg(); + $this->manualSetupKey = decrypt($user->two_factor_secret); + } catch (Exception) { + $this->addError('setupData', 'Failed to fetch setup data.'); + + $this->reset('qrCodeSvg', 'manualSetupKey'); + } + } + + /** + * Show the two-factor verification step if necessary. + */ + public function showVerificationIfNecessary(): void + { + if ($this->requiresConfirmation) { + $this->showVerificationStep = true; + + $this->resetErrorBag(); + + return; + } + + $this->closeModal(); + $this->dispatch('two-factor-enabled'); + } + + /** + * Confirm two-factor authentication for the user. + */ + public function confirmTwoFactor(ConfirmTwoFactorAuthentication $confirmTwoFactorAuthentication): void + { + $this->validate(); + + $confirmTwoFactorAuthentication(auth()->user(), $this->code); + + $this->setupComplete = true; + + $this->closeModal(); + + $this->dispatch('two-factor-enabled'); + } + + /** + * Reset two-factor verification state. + */ + public function resetVerification(): void + { + $this->reset('code', 'showVerificationStep'); + + $this->resetErrorBag(); + } + + /** + * Close the two-factor authentication modal. + */ + public function closeModal(): void + { + $this->reset( + 'code', + 'manualSetupKey', + 'qrCodeSvg', + 'showVerificationStep', + 'setupComplete', + ); + + $this->resetErrorBag(); + } + + /** + * Get the current modal configuration state. + */ + #[Computed] + public function modalConfig(): array + { + if ($this->setupComplete) { + return [ + 'title' => __('Two-factor authentication enabled'), + 'description' => __('Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.'), + 'buttonText' => __('Close'), + ]; + } + + if ($this->showVerificationStep) { + return [ + 'title' => __('Verify authentication code'), + 'description' => __('Enter the 6-digit code from your authenticator app.'), + 'buttonText' => __('Continue'), + ]; + } + + return [ + 'title' => __('Enable two-factor authentication'), + 'description' => __('To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app.'), + 'buttonText' => __('Continue'), + ]; + } +}; ?> + + +
+
+
+
+
+ @for ($i = 1; $i <= 5; $i++) +
+ @endfor +
+ +
+ @for ($i = 1; $i <= 5; $i++) +
+ @endfor +
+ + +
+
+ +
+ {{ $this->modalConfig['title'] }} + {{ $this->modalConfig['description'] }} +
+
+ + @if ($showVerificationStep) +
+
+ +
+ +
+ + {{ __('Back') }} + + + + {{ __('Confirm') }} + +
+
+ @else + @error('setupData') + + @enderror + +
+
+ @empty($qrCodeSvg) +
+ +
+ @else +
+
+ {!! $qrCodeSvg !!} +
+
+ @endempty +
+
+ +
+ + {{ $this->modalConfig['buttonText'] }} + +
+ +
+
+
+ + {{ __('or, enter the code manually') }} + +
+ +
+
+ @empty($manualSetupKey) +
+ +
+ @else + + + + @endempty +
+
+
+ @endif +
+
diff --git a/resources/views/pages/⚡dashboard.blade.php b/resources/views/pages/⚡dashboard.blade.php new file mode 100644 index 0000000..080d6db --- /dev/null +++ b/resources/views/pages/⚡dashboard.blade.php @@ -0,0 +1,199 @@ +reminder_months_before; + } + + /** + * @return array{total: int, valid: int, expiringSoon: int, expired: int} + */ + #[Computed] + public function stats(): array + { + $threshold = today()->addMonths($this->months()); + + $total = Certificate::count(); + $expired = Certificate::whereDate('expires_at', '<', today())->count(); + $expiringSoon = Certificate::whereDate('expires_at', '>=', today()) + ->whereDate('expires_at', '<=', $threshold) + ->count(); + + return [ + 'total' => $total, + 'valid' => max(0, $total - $expired - $expiringSoon), + 'expiringSoon' => $expiringSoon, + 'expired' => $expired, + ]; + } + + /** + * Certificates that are expired or expiring within the user's threshold, + * soonest first — the actionable items. + * + * @return Collection + */ + #[Computed] + public function attention(): Collection + { + return Certificate::with('category') + ->whereDate('expires_at', '<=', today()->addMonths($this->months())) + ->orderBy('expires_at') + ->take(6) + ->get(); + } + + /** + * @return Collection + */ + #[Computed] + public function recent(): Collection + { + return Certificate::with('category')->latest()->take(5)->get(); + } + + /** + * @return Collection + */ + #[Computed] + public function categories(): Collection + { + return Category::withCount('certificates') + ->orderByDesc('certificates_count') + ->orderBy('name') + ->get(); + } +}; ?> + +
+
+
+ {{ __('Welcome back, :name', ['name' => auth()->user()->name]) }} + {{ __('Here is the status of your certificates.') }} +
+ + {{ __('Add certificate') }} + +
+ + @if ($this->stats['total'] === 0) + + + {{ __('No certificates yet') }} + + {{ __('Upload your first certificate to start tracking issue dates, expiry, and reminders.') }} + + + {{ __('Add your first certificate') }} + + + @else + {{-- Stat tiles --}} +
+ @php + $tiles = [ + ['label' => __('Total'), 'value' => $this->stats['total'], 'icon' => 'rectangle-stack', 'class' => 'text-zinc-800 dark:text-white'], + ['label' => __('Valid'), 'value' => $this->stats['valid'], 'icon' => 'check-badge', 'class' => 'text-green-600 dark:text-green-400'], + ['label' => __('Expiring soon'), 'value' => $this->stats['expiringSoon'], 'icon' => 'clock', 'class' => 'text-amber-600 dark:text-amber-400'], + ['label' => __('Expired'), 'value' => $this->stats['expired'], 'icon' => 'exclamation-triangle', 'class' => 'text-red-600 dark:text-red-400'], + ]; + @endphp + + @foreach ($tiles as $tile) + +
+ {{ $tile['label'] }} + +
+ {{ $tile['value'] }} +
+ @endforeach +
+ +
+ {{-- Needs attention --}} + +
+ {{ __('Needs attention') }} + {{ __('View all') }} +
+ + @if ($this->attention->isEmpty()) +
+ + {{ __('Nothing needs attention. All your certificates are up to date.') }} +
+ @else +
+ @foreach ($this->attention as $certificate) + @php($status = $certificate->expiryStatus($this->stats['total'] ? auth()->user()->reminder_months_before : 1)) + +
+ {{ $certificate->title }} + {{ $certificate->category?->name ?? __('Uncategorized') }} +
+
+
+ {{ $certificate->expires_at->format('M j, Y') }} + {{ $certificate->expires_at->diffForHumans() }} +
+ {{ $status->label() }} +
+
+ @endforeach +
+ @endif +
+ +
+ {{-- By category --}} + +
+ {{ __('By category') }} + {{ __('Manage') }} +
+ + @if ($this->categories->isEmpty()) + {{ __('No categories yet.') }} + @else +
+ @foreach ($this->categories as $category) +
+ {{ $category->name }} + {{ $category->certificates_count }} +
+ @endforeach +
+ @endif +
+ + {{-- Recently added --}} + + {{ __('Recently added') }} +
+ @foreach ($this->recent as $certificate) + + {{ $certificate->title }} + {{ $certificate->created_at?->diffForHumans() }} + + @endforeach +
+
+
+
+ @endif +
diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php new file mode 100644 index 0000000..8e9a491 --- /dev/null +++ b/resources/views/partials/head.blade.php @@ -0,0 +1,15 @@ + + + + + {{ filled($title ?? null) ? $title.' - '.config('app.name', 'Laravel') : config('app.name', 'Laravel') }} + + + + + + +@fonts + +@vite(['resources/css/app.css', 'resources/js/app.js']) +@fluxAppearance diff --git a/resources/views/partials/settings-heading.blade.php b/resources/views/partials/settings-heading.blade.php new file mode 100644 index 0000000..925ace9 --- /dev/null +++ b/resources/views/partials/settings-heading.blade.php @@ -0,0 +1,5 @@ +
+ {{ __('Settings') }} + {{ __('Manage your profile and account settings') }} + +
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php new file mode 100644 index 0000000..13545d0 --- /dev/null +++ b/resources/views/welcome.blade.php @@ -0,0 +1,202 @@ + + + + + + + {{ __('Welcome') }} - {{ config('app.name', 'Laravel') }} + + + + + + @fonts + + + + + +
+ @if (Route::has('login')) + + @endif +
+
+
+
+

Let's get started

+

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

+ + +
+
+ {{-- Laravel Logo --}} + + + + + + + + + + + {{-- 13 --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + @if (Route::has('login')) + + @endif + + diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..8a9ce6a --- /dev/null +++ b/routes/console.php @@ -0,0 +1,11 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); + +Schedule::command('app:send-certificate-reminders')->dailyAt('08:00'); diff --git a/routes/settings.php b/routes/settings.php new file mode 100644 index 0000000..50752e9 --- /dev/null +++ b/routes/settings.php @@ -0,0 +1,28 @@ +group(function () { + Route::redirect('settings', 'settings/profile'); + + Route::livewire('settings/profile', 'pages::settings.profile')->name('profile.edit'); +}); + +Route::middleware(['auth', 'verified'])->group(function () { + Route::livewire('settings/appearance', 'pages::settings.appearance')->name('appearance.edit'); + + Route::livewire('settings/certificates', 'pages::settings.certificates')->name('certificate-settings.edit'); + + Route::livewire('settings/security', 'pages::settings.security') + ->middleware([ + 'password.confirm', + ]) + ->name('security.edit'); +}); + +Route::get('.well-known/passkey-endpoints', function () { + return response()->json([ + 'enroll' => route('security.edit'), + 'manage' => route('security.edit'), + ]); +})->name('well-known.passkeys'); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..29f7deb --- /dev/null +++ b/routes/web.php @@ -0,0 +1,32 @@ +name('home'); +Route::get('/security', [MarketingController::class, 'security'])->name('marketing.security'); +Route::get('/pricing', [MarketingController::class, 'pricing'])->name('marketing.pricing'); + +// Available to guests and authenticated users alike. +Route::get('language/{locale}', LanguageController::class)->name('language.switch'); + +Route::middleware(['auth', 'verified'])->group(function () { + Route::livewire('dashboard', 'pages::dashboard')->name('dashboard'); + + Route::livewire('certificates', 'pages::certificates.index')->name('certificates.index'); + Route::livewire('certificates/create', 'pages::certificates.create')->name('certificates.create'); + Route::livewire('certificates/{certificate}/edit', 'pages::certificates.edit')->name('certificates.edit'); + Route::get('certificates/{certificate}/download', CertificateDownloadController::class)->name('certificates.download'); + + Route::livewire('categories', 'pages::categories.index')->name('categories.index'); +}); + +// External recipients: authorized by the signed URL, not by login. +Route::get('shared/certificates/{certificate}', SharedCertificateController::class) + ->middleware('signed') + ->name('shared.certificate'); + +require __DIR__.'/settings.php'; diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 0000000..ebbe046 --- /dev/null +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,67 @@ +get(route('login')); + + $response->assertOk(); +}); + +test('users can authenticate using the login screen', function () { + $user = User::factory()->create(); + + $response = $this->post(route('login.store'), [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('dashboard', absolute: false)); + + $this->assertAuthenticated(); +}); + +test('users can not authenticate with invalid password', function () { + $user = User::factory()->create(); + + $response = $this->post(route('login.store'), [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrorsIn('email'); + + $this->assertGuest(); +}); + +test('users with two factor enabled are redirected to two factor challenge', function () { + $this->skipUnlessFortifyHas(Features::twoFactorAuthentication()); + + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + ]); + + $user = User::factory()->withTwoFactor()->create(); + + $response = $this->post(route('login.store'), [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $response->assertRedirect(route('two-factor.login')); + $this->assertGuest(); +}); + +test('users can logout', function () { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post(route('logout')); + + $response->assertRedirect(route('home')); + + $this->assertGuest(); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 0000000..e10227f --- /dev/null +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,72 @@ +skipUnlessFortifyHas(Features::emailVerification()); +}); + +test('email verification screen can be rendered', function () { + $user = User::factory()->unverified()->create(); + + $response = $this->actingAs($user)->get(route('verification.notice')); + + $response->assertOk(); +}); + +test('email can be verified', function () { + $user = User::factory()->unverified()->create(); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)], + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + + expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); + $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); +}); + +test('email is not verified with invalid hash', function () { + $user = User::factory()->unverified()->create(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')], + ); + + $this->actingAs($user)->get($verificationUrl); + + expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); +}); + +test('already verified user visiting verification link is redirected without firing event again', function () { + $user = User::factory()->create([ + 'email_verified_at' => now(), + ]); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)], + ); + + $this->actingAs($user)->get($verificationUrl) + ->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + + expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); + Event::assertNotDispatched(Verified::class); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 0000000..514115a --- /dev/null +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,11 @@ +create(); + + $response = $this->actingAs($user)->get(route('password.confirm')); + + $response->assertOk(); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000..586f8a7 --- /dev/null +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,65 @@ +skipUnlessFortifyHas(Features::resetPasswords()); +}); + +test('reset password link screen can be rendered', function () { + $response = $this->get(route('password.request')); + + $response->assertOk(); +}); + +test('reset password link can be requested', function () { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post(route('password.request'), ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class); +}); + +test('reset password screen can be rendered', function () { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post(route('password.request'), ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get(route('password.reset', $notification->token)); + + $response->assertOk(); + + return true; + }); +}); + +test('password can be reset with valid token', function () { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post(route('password.request'), ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $response = $this->post(route('password.update'), [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('login', absolute: false)); + + return true; + }); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..874e6fa --- /dev/null +++ b/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,27 @@ +skipUnlessFortifyHas(Features::registration()); +}); + +test('registration screen can be rendered', function () { + $response = $this->get(route('register')); + + $response->assertOk(); +}); + +test('new users can register', function () { + $response = $this->post(route('register.store'), [ + 'name' => 'John Doe', + 'email' => 'test@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response->assertSessionHasNoErrors() + ->assertRedirect(route('dashboard', absolute: false)); + + $this->assertAuthenticated(); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/TwoFactorChallengeTest.php b/tests/Feature/Auth/TwoFactorChallengeTest.php new file mode 100644 index 0000000..2dc7a6a --- /dev/null +++ b/tests/Feature/Auth/TwoFactorChallengeTest.php @@ -0,0 +1,28 @@ +skipUnlessFortifyHas(Features::twoFactorAuthentication()); +}); + +test('two factor challenge redirects to login when not authenticated', function () { + $response = $this->get(route('two-factor.login')); + + $response->assertRedirect(route('login')); +}); + +test('two factor challenge can be rendered', function () { + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + ]); + + $user = User::factory()->withTwoFactor()->create(); + + $this->post(route('login.store'), [ + 'email' => $user->email, + 'password' => 'password', + ])->assertRedirect(route('two-factor.login')); +}); \ No newline at end of file diff --git a/tests/Feature/Categories/CategoryManagementTest.php b/tests/Feature/Categories/CategoryManagementTest.php new file mode 100644 index 0000000..9dff7c5 --- /dev/null +++ b/tests/Feature/Categories/CategoryManagementTest.php @@ -0,0 +1,87 @@ +get(route('categories.index'))->assertRedirect(route('login')); +}); + +test('authenticated users can view the categories page', function () { + $this->actingAs(User::factory()->create()); + + $this->get(route('categories.index'))->assertOk(); +}); + +test('a user can create a category', function () { + $this->actingAs($user = User::factory()->create()); + + Livewire::test('pages::categories.index') + ->set('name', 'Work') + ->call('save') + ->assertHasNoErrors(); + + expect($user->categories()->where('name', 'Work')->exists())->toBeTrue(); +}); + +test('category names must be unique per user', function () { + $this->actingAs($user = User::factory()->create()); + + Category::factory()->for($user)->create(['name' => 'Work']); + + Livewire::test('pages::categories.index') + ->set('name', 'Work') + ->call('save') + ->assertHasErrors(['name']); +}); + +test('different users can use the same category name', function () { + Category::factory()->create(['name' => 'Work']); + + $this->actingAs(User::factory()->create()); + + Livewire::test('pages::categories.index') + ->set('name', 'Work') + ->call('save') + ->assertHasNoErrors(); +}); + +test('a category name is required', function () { + $this->actingAs(User::factory()->create()); + + Livewire::test('pages::categories.index') + ->set('name', '') + ->call('save') + ->assertHasErrors(['name' => 'required']); +}); + +test('deleting a category keeps its certificates as uncategorized', function () { + $this->actingAs($user = User::factory()->create()); + + $category = Category::factory()->for($user)->create(); + $certificate = Certificate::factory()->for($user)->create(['category_id' => $category->id]); + + Livewire::test('pages::categories.index') + ->call('delete', $category->id); + + expect(Category::find($category->id))->toBeNull() + ->and($certificate->refresh()->category_id)->toBeNull(); +}); + +test('a user cannot delete another users category', function () { + $otherCategory = Category::factory()->create(); + + $this->actingAs(User::factory()->create()); + + try { + Livewire::test('pages::categories.index')->call('delete', $otherCategory->id); + $this->fail('Expected ModelNotFoundException was not thrown.'); + } catch (ModelNotFoundException) { + // Expected: the global scope hides foreign categories. + } + + expect(Category::withoutGlobalScopes()->find($otherCategory->id))->not->toBeNull(); +}); diff --git a/tests/Feature/Certificates/CertificateActionsTest.php b/tests/Feature/Certificates/CertificateActionsTest.php new file mode 100644 index 0000000..2547fa5 --- /dev/null +++ b/tests/Feature/Certificates/CertificateActionsTest.php @@ -0,0 +1,73 @@ +id.'/'.uniqid().'.pdf'; + Storage::disk('local')->put($path, 'file-contents'); + + return Certificate::factory()->for($user)->create(array_merge(['file_path' => $path], $attributes)); +} + +test('an owner can download their certificate', function () { + $user = User::factory()->create(); + $certificate = storedCertificate($user); + + $this->actingAs($user); + + $this->get(route('certificates.download', $certificate))->assertOk(); +}); + +test('a user cannot download another users certificate', function () { + $certificate = storedCertificate(User::factory()->create()); + + $this->actingAs(User::factory()->create()); + + $this->get(route('certificates.download', $certificate))->assertNotFound(); +}); + +test('editing a certificate updates its details', function () { + $user = User::factory()->create(); + $certificate = storedCertificate($user, ['title' => 'Old']); + + $this->actingAs($user); + + Livewire::test('pages::certificates.edit', ['certificate' => $certificate]) + ->set('title', 'New Title') + ->set('issuer', 'New Issuer') + ->set('certificate_number', 'NR-42') + ->set('issued_at', now()->subYear()->toDateString()) + ->set('expires_at', now()->addYear()->toDateString()) + ->set('notes', 'Updated notes.') + ->call('save') + ->assertHasNoErrors(); + + $certificate->refresh(); + + expect($certificate->title)->toBe('New Title') + ->and($certificate->issuer)->toBe('New Issuer') + ->and($certificate->certificate_number)->toBe('NR-42') + ->and($certificate->notes)->toBe('Updated notes.'); +}); + +test('deleting a certificate removes its stored file', function () { + $user = User::factory()->create(); + $certificate = storedCertificate($user); + $path = $certificate->file_path; + + $this->actingAs($user); + + Livewire::test('pages::certificates.index') + ->call('delete', $certificate->id); + + expect(Certificate::find($certificate->id))->toBeNull(); + Storage::disk('local')->assertMissing($path); +}); diff --git a/tests/Feature/Certificates/CertificateIndexTest.php b/tests/Feature/Certificates/CertificateIndexTest.php new file mode 100644 index 0000000..8fa01a9 --- /dev/null +++ b/tests/Feature/Certificates/CertificateIndexTest.php @@ -0,0 +1,117 @@ +get(route('certificates.index'))->assertRedirect(route('login')); +}); + +test('authenticated users can view the certificates page', function () { + $this->actingAs(User::factory()->create()); + + $this->get(route('certificates.index'))->assertOk(); +}); + +test('users only see their own certificates', function () { + $user = User::factory()->create(); + $other = User::factory()->create(); + + Certificate::factory()->for($user)->create(['title' => 'My Own Diploma']); + Certificate::factory()->for($other)->create(['title' => 'Foreign Diploma']); + + $this->actingAs($user); + + $this->get(route('certificates.index')) + ->assertSee('My Own Diploma') + ->assertDontSee('Foreign Diploma'); +}); + +test('certificates can be filtered by category', function () { + $this->actingAs($user = User::factory()->create()); + + $category = Category::factory()->for($user)->create(); + Certificate::factory()->for($user)->create(['title' => 'Categorized One', 'category_id' => $category->id]); + Certificate::factory()->for($user)->create(['title' => 'Uncategorized One']); + + Livewire::test('pages::certificates.index') + ->set('categoryFilter', (string) $category->id) + ->assertSee('Categorized One') + ->assertDontSee('Uncategorized One'); +}); + +test('certificates can be sorted by title in both directions', function () { + $this->actingAs($user = User::factory()->create()); + + Certificate::factory()->for($user)->create(['title' => 'Alpha Certificate']); + Certificate::factory()->for($user)->create(['title' => 'Zulu Certificate']); + + Livewire::test('pages::certificates.index') + ->call('sort', 'title') + ->assertSet('sortBy', 'title') + ->assertSet('sortDirection', 'asc') + ->assertSeeInOrder(['Alpha Certificate', 'Zulu Certificate']) + ->call('sort', 'title') + ->assertSet('sortDirection', 'desc') + ->assertSeeInOrder(['Zulu Certificate', 'Alpha Certificate']); +}); + +test('certificates are sorted by expiry date by default', function () { + $this->actingAs($user = User::factory()->create()); + + Certificate::factory()->for($user)->create(['title' => 'Later Cert', 'expires_at' => now()->addYears(2)]); + Certificate::factory()->for($user)->create(['title' => 'Sooner Cert', 'expires_at' => now()->addMonths(3)]); + + Livewire::test('pages::certificates.index') + ->assertSet('sortBy', 'expires_at') + ->assertSeeInOrder(['Sooner Cert', 'Later Cert']); +}); + +test('invalid sort columns are ignored', function () { + $this->actingAs(User::factory()->create()); + + Livewire::test('pages::certificates.index') + ->call('sort', 'user_id') + ->assertSet('sortBy', 'expires_at'); +}); + +test('sorting and filtering are initialized from the query string', function () { + $this->actingAs(User::factory()->create()); + + Livewire::withQueryParams(['sort' => 'title', 'direction' => 'desc']) + ->test('pages::certificates.index') + ->assertSet('sortBy', 'title') + ->assertSet('sortDirection', 'desc'); +}); + +test('the certificate list is paginated', function () { + $this->actingAs($user = User::factory()->create()); + + Certificate::factory()->for($user)->count(15)->create(); + + $certificates = Livewire::test('pages::certificates.index')->instance()->certificates; + + expect($certificates->count())->toBe(10) + ->and($certificates->total())->toBe(15); +}); + +test('changing the category filter resets pagination', function () { + $this->actingAs($user = User::factory()->create()); + + Certificate::factory()->for($user)->count(15)->create(); + + Livewire::test('pages::certificates.index') + ->call('nextPage') + ->set('categoryFilter', '') + ->assertSet('paginators.page', 1); +}); + +test('expired certificates show an expired badge', function () { + $this->actingAs($user = User::factory()->create()); + + Certificate::factory()->for($user)->expired()->create(); + + $this->get(route('certificates.index'))->assertSee('Expired'); +}); diff --git a/tests/Feature/Certificates/CertificateReminderTest.php b/tests/Feature/Certificates/CertificateReminderTest.php new file mode 100644 index 0000000..9b3de19 --- /dev/null +++ b/tests/Feature/Certificates/CertificateReminderTest.php @@ -0,0 +1,51 @@ +create(['reminder_months_before' => 1]); + $certificate = Certificate::factory()->for($user)->create([ + 'expires_at' => today()->addMonth(), + ]); + + $this->artisan('app:send-certificate-reminders')->assertSuccessful(); + + Notification::assertSentTo($user, CertificateExpiringNotification::class); + expect($certificate->refresh()->last_reminded_at)->not->toBeNull(); +}); + +test('an urgent reminder is sent for certificates expiring today', function () { + $user = User::factory()->create(); + Certificate::factory()->for($user)->create(['expires_at' => today()]); + + $this->artisan('app:send-certificate-reminders')->assertSuccessful(); + + Notification::assertSentTo($user, CertificateExpiredNotification::class); +}); + +test('certificates outside the threshold are not notified', function () { + $user = User::factory()->create(['reminder_months_before' => 1]); + Certificate::factory()->for($user)->create(['expires_at' => today()->addMonths(6)]); + + $this->artisan('app:send-certificate-reminders')->assertSuccessful(); + + Notification::assertNothingSent(); +}); + +test('reminders are not sent twice on the same day', function () { + $user = User::factory()->create(['reminder_months_before' => 1]); + Certificate::factory()->for($user)->create(['expires_at' => today()->addMonth()]); + + $this->artisan('app:send-certificate-reminders')->assertSuccessful(); + $this->artisan('app:send-certificate-reminders')->assertSuccessful(); + + Notification::assertSentToTimes($user, CertificateExpiringNotification::class, 1); +}); diff --git a/tests/Feature/Certificates/CertificateSharingTest.php b/tests/Feature/Certificates/CertificateSharingTest.php new file mode 100644 index 0000000..b1aa851 --- /dev/null +++ b/tests/Feature/Certificates/CertificateSharingTest.php @@ -0,0 +1,105 @@ +id.'/'.uniqid().'.pdf'; + Storage::disk('local')->put($path, 'file-contents'); + + return Certificate::factory()->for($user)->create(['file_path' => $path]); +} + +test('sharing by secure link emails the recipient', function () { + $user = User::factory()->create(); + $certificate = sharableCertificate($user); + + $this->actingAs($user); + + Livewire::test('pages::certificates.index') + ->set('selected', [$certificate->id]) + ->set('shareEmail', 'friend@example.com') + ->set('shareMethod', 'link') + ->call('share') + ->assertHasNoErrors(); + + Mail::assertSent(SharedCertificateLinksMail::class, fn ($mail) => $mail->hasTo('friend@example.com')); +}); + +test('sharing by zip emails an attachment and reveals a password', function () { + $user = User::factory()->create(); + $certificate = sharableCertificate($user); + + $this->actingAs($user); + + $component = Livewire::test('pages::certificates.index') + ->set('selected', [$certificate->id]) + ->set('shareEmail', 'friend@example.com') + ->set('shareMethod', 'zip') + ->call('share') + ->assertHasNoErrors(); + + expect($component->get('sharePassword'))->toHaveLength(12); + + Mail::assertSent(SharedCertificatesMail::class, fn ($mail) => $mail->hasTo('friend@example.com')); +}); + +test('sharing requires a recipient and a selection', function () { + $this->actingAs(User::factory()->create()); + + Livewire::test('pages::certificates.index') + ->set('shareEmail', '') + ->set('selected', []) + ->call('share') + ->assertHasErrors(['shareEmail', 'selected']); + + Mail::assertNothingSent(); +}); + +test('sharing is rate limited', function () { + $user = User::factory()->create(); + $certificate = sharableCertificate($user); + + $this->actingAs($user); + + for ($i = 0; $i < 10; $i++) { + RateLimiter::hit('share-certificates:'.$user->id, 3600); + } + + Livewire::test('pages::certificates.index') + ->set('selected', [$certificate->id]) + ->set('shareEmail', 'friend@example.com') + ->call('share') + ->assertHasErrors(['shareEmail']); + + Mail::assertNothingSent(); +}); + +test('a valid signed link streams the file to an unauthenticated recipient', function () { + $user = User::factory()->create(); + $certificate = sharableCertificate($user); + + $url = URL::temporarySignedRoute('shared.certificate', now()->addHours(48), ['certificate' => $certificate->id]); + + $this->get($url)->assertOk(); +}); + +test('a tampered signed link is rejected', function () { + $user = User::factory()->create(); + $certificate = sharableCertificate($user); + + $this->get(route('shared.certificate', $certificate))->assertForbidden(); +}); diff --git a/tests/Feature/Certificates/CertificateUploadTest.php b/tests/Feature/Certificates/CertificateUploadTest.php new file mode 100644 index 0000000..c31c091 --- /dev/null +++ b/tests/Feature/Certificates/CertificateUploadTest.php @@ -0,0 +1,123 @@ +actingAs(User::factory()->create()); + + $this->get(route('certificates.create'))->assertOk(); +}); + +test('a certificate is stored on the private disk', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Livewire::test('pages::certificates.create') + ->set('file', UploadedFile::fake()->create('cert.pdf', 200, 'application/pdf')) + ->set('title', 'My Diploma') + ->set('expires_at', now()->addYear()->toDateString()) + ->call('save') + ->assertHasNoErrors() + ->assertRedirect(route('certificates.index')); + + $certificate = $user->certificates()->firstOrFail(); + + expect($certificate->title)->toBe('My Diploma') + ->and($certificate->file_path)->toStartWith('certificates/'.$user->id.'/'); + + Storage::disk('local')->assertExists($certificate->file_path); +}); + +test('certificate details are stored', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Livewire::test('pages::certificates.create') + ->set('file', UploadedFile::fake()->create('cert.pdf', 200, 'application/pdf')) + ->set('title', 'First Aid') + ->set('issuer', 'Red Cross') + ->set('certificate_number', 'RC-2026-001') + ->set('issued_at', '2026-01-10') + ->set('expires_at', '2028-01-10') + ->set('notes', 'Renewed on-site.') + ->call('save') + ->assertHasNoErrors(); + + $certificate = $user->certificates()->firstOrFail(); + + expect($certificate->issuer)->toBe('Red Cross') + ->and($certificate->certificate_number)->toBe('RC-2026-001') + ->and($certificate->issued_at->toDateString())->toBe('2026-01-10') + ->and($certificate->notes)->toBe('Renewed on-site.'); +}); + +test('the issue date cannot be after the expiry date', function () { + $this->actingAs(User::factory()->create()); + + Livewire::test('pages::certificates.create') + ->set('file', UploadedFile::fake()->create('cert.pdf', 200, 'application/pdf')) + ->set('title', 'Bad Dates') + ->set('issued_at', '2028-01-10') + ->set('expires_at', '2026-01-10') + ->call('save') + ->assertHasErrors(['issued_at']); +}); + +test('an invalid file type is rejected', function () { + $this->actingAs(User::factory()->create()); + + Livewire::test('pages::certificates.create') + ->set('file', UploadedFile::fake()->create('malware.exe', 100)) + ->assertHasErrors(['file']); +}); + +test('ocr suggestions pre-fill empty fields on upload', function () { + Http::fake([ + 'custom-ocr.klippa.com/*' => Http::response([ + 'data' => ['parsed' => [ + 'document_subject' => 'Scanned Title', + 'expiry_date' => '2028-01-15', + ]], + ]), + ]); + + $user = User::factory()->create(); + $user->ocr_provider = 'klippa'; + $user->ocr_api_key = 'key'; + $user->save(); + + $this->actingAs($user); + + Livewire::test('pages::certificates.create') + ->set('file', UploadedFile::fake()->create('cert.pdf', 200, 'application/pdf')) + ->assertSet('title', 'Scanned Title') + ->assertSet('expires_at', '2028-01-15'); +}); + +test('ocr does not overwrite fields the user already filled', function () { + Http::fake([ + 'custom-ocr.klippa.com/*' => Http::response([ + 'data' => ['parsed' => ['document_subject' => 'Scanned Title']], + ]), + ]); + + $user = User::factory()->create(); + $user->ocr_provider = 'klippa'; + $user->ocr_api_key = 'key'; + $user->save(); + + $this->actingAs($user); + + Livewire::test('pages::certificates.create') + ->set('title', 'My Own Title') + ->set('file', UploadedFile::fake()->create('cert.pdf', 200, 'application/pdf')) + ->assertSet('title', 'My Own Title'); +}); diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php new file mode 100644 index 0000000..0fb04b5 --- /dev/null +++ b/tests/Feature/DashboardTest.php @@ -0,0 +1,57 @@ +get(route('dashboard')); + $response->assertRedirect(route('login')); +}); + +test('authenticated users can visit the dashboard', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('dashboard')); + $response->assertOk(); +}); + +test('the dashboard computes certificate stats for the user', function () { + $user = User::factory()->create(['reminder_months_before' => 1]); + + Certificate::factory()->for($user)->create(['expires_at' => today()->addYear()]); // valid + Certificate::factory()->for($user)->create(['expires_at' => today()->addWeeks(2)]); // expiring soon + Certificate::factory()->for($user)->expired()->create(); // expired + + $this->actingAs($user); + + Livewire::test('pages::dashboard') + ->assertSet('stats', fn ($stats) => $stats['total'] === 3 + && $stats['valid'] === 1 + && $stats['expiringSoon'] === 1 + && $stats['expired'] === 1); +}); + +test('the dashboard only reflects the authenticated user data', function () { + $user = User::factory()->create(); + Certificate::factory()->for($user)->create(['title' => 'My Certificate', 'expires_at' => today()->addWeek()]); + + $other = User::factory()->create(); + Certificate::factory()->for($other)->create(['title' => 'Someone Elses', 'expires_at' => today()->addWeek()]); + + $this->actingAs($user); + + $this->get(route('dashboard')) + ->assertOk() + ->assertSee('My Certificate') + ->assertDontSee('Someone Elses'); +}); + +test('the dashboard shows an empty state with no certificates', function () { + $this->actingAs(User::factory()->create()); + + $this->get(route('dashboard')) + ->assertOk() + ->assertSee('No certificates yet'); +}); \ No newline at end of file diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..a576279 --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,7 @@ +get(route('home')); + + $response->assertOk(); +}); \ No newline at end of file diff --git a/tests/Feature/LocaleTest.php b/tests/Feature/LocaleTest.php new file mode 100644 index 0000000..3a1da8d --- /dev/null +++ b/tests/Feature/LocaleTest.php @@ -0,0 +1,50 @@ +get('/language/nl'); + + $response->assertRedirect(); + expect(session('locale'))->toBe('nl'); +}); + +test('switching to an unsupported locale is rejected', function () { + $this->get('/language/de')->assertNotFound(); + + expect(session('locale'))->toBeNull(); +}); + +test('switching the language persists it on the authenticated user', function () { + $user = User::factory()->create(['locale' => 'en']); + $this->actingAs($user); + + $this->get('/language/nl')->assertRedirect(); + + expect($user->refresh()->locale)->toBe('nl'); +}); + +test("a guest's session locale is applied to the rendered page", function () { + $this->withSession(['locale' => 'nl']); + + $this->get(route('login')) + ->assertOk() + ->assertSee('Inloggen') + ->assertSee('Wachtwoord vergeten'); +}); + +test("an authenticated user's saved locale is applied even without a session value", function () { + $user = User::factory()->create(['locale' => 'nl']); + $this->actingAs($user); + + $this->get(route('dashboard')) + ->assertOk() + ->assertSee('Certificaten'); +}); + +test('the default locale is english', function () { + $this->get(route('login')) + ->assertOk() + ->assertSee('Log in') + ->assertDontSee('Inloggen'); +}); diff --git a/tests/Feature/Models/OwnershipTest.php b/tests/Feature/Models/OwnershipTest.php new file mode 100644 index 0000000..fa6e463 --- /dev/null +++ b/tests/Feature/Models/OwnershipTest.php @@ -0,0 +1,81 @@ +actingAs($user = User::factory()->create()); + + $certificate = Certificate::create([ + 'title' => 'Forklift License', + 'expires_at' => now()->addYear(), + ]); + + expect($certificate->user_id)->toBe($user->id); +}); + +test('certificate queries only return the authenticated users records', function () { + $user = User::factory()->create(); + $other = User::factory()->create(); + + Certificate::factory()->for($user)->create(['title' => 'Mine']); + Certificate::factory()->for($other)->create(['title' => 'Theirs']); + + $this->actingAs($user); + + expect(Certificate::all())->toHaveCount(1) + ->and(Certificate::first()->title)->toBe('Mine') + ->and(Certificate::withoutGlobalScope(OwnedByUserScope::class)->count())->toBe(2); +}); + +test('category queries only return the authenticated users records', function () { + $user = User::factory()->create(); + $other = User::factory()->create(); + + Category::factory()->for($user)->create(['name' => 'Mine']); + Category::factory()->for($other)->create(['name' => 'Theirs']); + + $this->actingAs($user); + + expect(Category::all())->toHaveCount(1) + ->and(Category::first()->name)->toBe('Mine'); +}); + +test('finding another users certificate throws a model not found exception', function () { + $other = User::factory()->create(); + $certificate = Certificate::factory()->for($other)->create(); + + $this->actingAs(User::factory()->create()); + + Certificate::findOrFail($certificate->id); +})->throws(ModelNotFoundException::class); + +test('ocr api key is encrypted at rest and hidden from serialization', function () { + $user = User::factory()->create(); + $user->ocr_api_key = 'secret-api-key'; + $user->save(); + + $raw = DB::table('users')->where('id', $user->id)->value('ocr_api_key'); + + expect($raw)->not->toBe('secret-api-key') + ->and($user->refresh()->ocr_api_key)->toBe('secret-api-key') + ->and($user->toArray())->not->toHaveKey('ocr_api_key'); +}); + +test('expiry status reflects the expiry date and reminder threshold', function () { + $this->actingAs(User::factory()->create()); + + $expired = Certificate::factory()->create(['expires_at' => now()->subDay()]); + $expiringSoon = Certificate::factory()->create(['expires_at' => now()->addWeeks(2)]); + $valid = Certificate::factory()->create(['expires_at' => now()->addYear()]); + + expect($expired->expiryStatus(1))->toBe(ExpiryStatus::Expired) + ->and($expiringSoon->expiryStatus(1))->toBe(ExpiryStatus::ExpiringSoon) + ->and($valid->expiryStatus(1))->toBe(ExpiryStatus::Valid) + ->and($valid->expiryStatus(24))->toBe(ExpiryStatus::ExpiringSoon); +}); diff --git a/tests/Feature/Ocr/OcrProviderTest.php b/tests/Feature/Ocr/OcrProviderTest.php new file mode 100644 index 0000000..64aa8b2 --- /dev/null +++ b/tests/Feature/Ocr/OcrProviderTest.php @@ -0,0 +1,82 @@ +driver('none'))->toBeInstanceOf(NoneProvider::class) + ->and($manager->driver('klippa'))->toBeInstanceOf(KlippaProvider::class) + ->and($manager->driver('mistral'))->toBeInstanceOf(MistralProvider::class) + ->and($manager->driver())->toBeInstanceOf(NoneProvider::class); +}); + +test('the none provider returns no suggestions', function () { + expect((new NoneProvider)->analyze('/tmp/whatever.pdf', null))->toBe([]); +}); + +test('providers return no suggestions without an api key', function () { + Http::fake(); + + expect((new KlippaProvider)->analyze(__FILE__, null))->toBe([]) + ->and((new MistralProvider)->analyze(__FILE__, ''))->toBe([]); + + Http::assertNothingSent(); +}); + +test('the klippa provider maps parsed fields to suggestions', function () { + Http::fake([ + 'custom-ocr.klippa.com/*' => Http::response([ + 'data' => ['parsed' => [ + 'document_subject' => 'Forklift Licence', + 'expiry_date' => '2027-05-01', + ]], + ]), + ]); + + $file = tempnam(sys_get_temp_dir(), 'ocr').'.pdf'; + file_put_contents($file, 'dummy'); + + $result = (new KlippaProvider)->analyze($file, 'test-key'); + + expect($result)->toBe(['title' => 'Forklift Licence', 'expires_at' => '2027-05-01']); + + @unlink($file); +}); + +test('the mistral provider parses json content from a vision response', function () { + Http::fake([ + 'api.mistral.ai/*' => Http::response([ + 'choices' => [[ + 'message' => ['content' => '{"title":"First Aid","expires_at":"2026-12-31"}'], + ]], + ]), + ]); + + $file = tempnam(sys_get_temp_dir(), 'ocr').'.png'; + // Minimal 1x1 PNG so mime detection reports an image. + file_put_contents($file, base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==')); + + $result = (new MistralProvider)->analyze($file, 'test-key'); + + expect($result)->toBe(['title' => 'First Aid', 'expires_at' => '2026-12-31']); + + @unlink($file); +}); + +test('the mistral provider skips non-image files', function () { + Http::fake(); + + $file = tempnam(sys_get_temp_dir(), 'ocr').'.pdf'; + file_put_contents($file, '%PDF-1.4 dummy'); + + expect((new MistralProvider)->analyze($file, 'test-key'))->toBe([]); + + Http::assertNothingSent(); + + @unlink($file); +}); diff --git a/tests/Feature/Settings/CertificateSettingsTest.php b/tests/Feature/Settings/CertificateSettingsTest.php new file mode 100644 index 0000000..b343319 --- /dev/null +++ b/tests/Feature/Settings/CertificateSettingsTest.php @@ -0,0 +1,55 @@ +actingAs(User::factory()->create()); + + $this->get(route('certificate-settings.edit'))->assertOk(); +}); + +test('a user can update their reminder and ocr settings', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + Livewire::test('pages::settings.certificates') + ->set('reminder_months_before', 3) + ->set('ocr_provider', 'klippa') + ->set('ocr_api_key', 'secret-key') + ->call('updateSettings') + ->assertHasNoErrors(); + + $user->refresh(); + + expect($user->reminder_months_before)->toBe(3) + ->and($user->ocr_provider)->toBe('klippa') + ->and($user->ocr_api_key)->toBe('secret-key'); +}); + +test('a blank api key keeps the existing one', function () { + $user = User::factory()->create(); + $user->ocr_provider = 'klippa'; + $user->ocr_api_key = 'original-key'; + $user->save(); + + $this->actingAs($user); + + Livewire::test('pages::settings.certificates') + ->set('reminder_months_before', 2) + ->set('ocr_provider', 'klippa') + ->set('ocr_api_key', '') + ->call('updateSettings') + ->assertHasNoErrors(); + + expect($user->refresh()->ocr_api_key)->toBe('original-key'); +}); + +test('an unknown ocr provider is rejected', function () { + $this->actingAs(User::factory()->create()); + + Livewire::test('pages::settings.certificates') + ->set('ocr_provider', 'hacker') + ->call('updateSettings') + ->assertHasErrors(['ocr_provider']); +}); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php new file mode 100644 index 0000000..e0b3742 --- /dev/null +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -0,0 +1,75 @@ +actingAs($user = User::factory()->create()); + + $this->get(route('profile.edit'))->assertOk(); +}); + +test('profile information can be updated', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $response = Livewire::test('pages::settings.profile') + ->set('name', 'Test User') + ->set('email', 'test@example.com') + ->call('updateProfileInformation'); + + $response->assertHasNoErrors(); + + $user->refresh(); + + expect($user->name)->toEqual('Test User'); + expect($user->email)->toEqual('test@example.com'); + expect($user->email_verified_at)->toBeNull(); +}); + +test('email verification status is unchanged when email address is unchanged', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $response = Livewire::test('pages::settings.profile') + ->set('name', 'Test User') + ->set('email', $user->email) + ->call('updateProfileInformation'); + + $response->assertHasNoErrors(); + + expect($user->refresh()->email_verified_at)->not->toBeNull(); +}); + +test('user can delete their account', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $response = Livewire::test('pages::settings.delete-user-modal') + ->set('password', 'password') + ->call('deleteUser'); + + $response + ->assertHasNoErrors() + ->assertRedirect('/'); + + expect($user->fresh())->toBeNull(); + expect(auth()->check())->toBeFalse(); +}); + +test('correct password must be provided to delete account', function () { + $user = User::factory()->create(); + + $this->actingAs($user); + + $response = Livewire::test('pages::settings.delete-user-modal') + ->set('password', 'wrong-password') + ->call('deleteUser'); + + $response->assertHasErrors(['password']); + + expect($user->fresh())->not->toBeNull(); +}); \ No newline at end of file diff --git a/tests/Feature/Settings/SecurityTest.php b/tests/Feature/Settings/SecurityTest.php new file mode 100644 index 0000000..f85732c --- /dev/null +++ b/tests/Feature/Settings/SecurityTest.php @@ -0,0 +1,113 @@ +skipUnlessFortifyHas(Features::twoFactorAuthentication()); + + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + ]); + Features::passkeys([ + 'confirmPassword' => true, + ]); +}); + +test('security settings page can be rendered', function () { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->withSession(['auth.password_confirmed_at' => time()]) + ->get(route('security.edit')); + + $response->assertOk(); + + $response->assertSee('Passkeys'); + $response->assertSee('No passkeys yet'); + $response->assertSee('Two-factor authentication'); + $response->assertSee('Enable 2FA'); +}); + +test('security settings page requires password confirmation when enabled', function () { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->get(route('security.edit')); + + $response->assertRedirect(route('password.confirm')); +}); + +test('security settings page renders without two factor when feature is disabled', function () { + config(['fortify.features' => []]); + + $user = User::factory()->create(); + + $this->actingAs($user) + ->withSession(['auth.password_confirmed_at' => time()]) + ->get(route('security.edit')) + ->assertOk() + ->assertSee('Update password') + ->assertDontSee('Manage your passkeys for passwordless sign-in') + ->assertDontSee('Add a passkey to sign in without a password') + ->assertDontSee('Two-factor authentication'); +}); + +test('two factor authentication disabled when confirmation abandoned between requests', function () { + $user = User::factory()->create(); + + $user->forceFill([ + 'two_factor_secret' => encrypt('test-secret'), + 'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])), + 'two_factor_confirmed_at' => null, + ])->save(); + + $this->actingAs($user); + + $component = Livewire::test('pages::settings.security'); + + $component->assertSet('twoFactorEnabled', false); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'two_factor_secret' => null, + 'two_factor_recovery_codes' => null, + ]); +}); + +test('password can be updated', function () { + $user = User::factory()->create([ + 'password' => Hash::make('password'), + ]); + + $this->actingAs($user); + + $response = Livewire::test('pages::settings.security') + ->set('current_password', 'password') + ->set('password', 'new-password') + ->set('password_confirmation', 'new-password') + ->call('updatePassword'); + + $response->assertHasNoErrors(); + + expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue(); +}); + +test('correct password must be provided to update password', function () { + $user = User::factory()->create([ + 'password' => Hash::make('password'), + ]); + + $this->actingAs($user); + + $response = Livewire::test('pages::settings.security') + ->set('current_password', 'wrong-password') + ->set('password', 'new-password') + ->set('password_confirmation', 'new-password') + ->call('updatePassword'); + + $response->assertHasErrors(['current_password']); +}); \ No newline at end of file diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..941e024 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,50 @@ +extend(TestCase::class) + ->use(RefreshDatabase::class) + ->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..a574f37 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,16 @@ +markTestSkipped($message ?? "Fortify feature [{$feature}] is not enabled."); + } + } +} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php new file mode 100644 index 0000000..27f3f87 --- /dev/null +++ b/tests/Unit/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..b512d9a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,32 @@ +import { + defineConfig +} from 'vite'; +import laravel from 'laravel-vite-plugin'; +import { bunny } from 'laravel-vite-plugin/fonts'; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [ + laravel({ + input: [ + 'resources/css/app.css', + 'resources/js/app.js', + 'resources/js/passkeys.js', + 'resources/js/marketing.js', + ], + refresh: true, + fonts: [ + bunny('Instrument Sans', { + weights: [400, 500, 600, 700], + }), + ], + }), + tailwindcss(), + ], + server: { + cors: true, + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +});