diff --git a/.env.example b/.env.example index c3b8c94d8f7..a3c3e3dc142 100644 --- a/.env.example +++ b/.env.example @@ -180,6 +180,15 @@ VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" # On non-SE installations the flag is ignored and Lychee branding remains visible. # WHITE_LABEL_ENABLED=false +################################################################### +# Keygen License Management # +################################################################### + +# API token obtained from keygen.lycheeorg.dev. +# When set, Lychee will automatically rotate an expired license key +# on admin login and check the token health on the diagnostics page. +# KEYGEN_API_KEY= + ################################################################### # LDAP Authentication (enterprise directory integration) # ################################################################### diff --git a/app/Actions/Diagnostics/Errors.php b/app/Actions/Diagnostics/Errors.php index 40192b17fb5..ae3c93b2476 100644 --- a/app/Actions/Diagnostics/Errors.php +++ b/app/Actions/Diagnostics/Errors.php @@ -27,6 +27,7 @@ use App\Actions\Diagnostics\Pipes\Checks\ImageOptCheck; use App\Actions\Diagnostics\Pipes\Checks\ImagickPdfCheck; use App\Actions\Diagnostics\Pipes\Checks\IniSettingsCheck; +use App\Actions\Diagnostics\Pipes\Checks\KeygenApiTokenCheck; use App\Actions\Diagnostics\Pipes\Checks\MigrationCheck; use App\Actions\Diagnostics\Pipes\Checks\OldLicenseCheck; use App\Actions\Diagnostics\Pipes\Checks\OpCacheCheck; @@ -57,6 +58,7 @@ class Errors BasicPermissionCheck::class, ConfigSanityCheck::class, OldLicenseCheck::class, + KeygenApiTokenCheck::class, DBSupportCheck::class, GDSupportCheck::class, ImageOptCheck::class, diff --git a/app/Actions/Diagnostics/Pipes/Checks/KeygenApiTokenCheck.php b/app/Actions/Diagnostics/Pipes/Checks/KeygenApiTokenCheck.php new file mode 100644 index 00000000000..7e9abe28c38 --- /dev/null +++ b/app/Actions/Diagnostics/Pipes/Checks/KeygenApiTokenCheck.php @@ -0,0 +1,68 @@ +may_administrate !== true) { + return $next($data); + } + + /** @var string $api_key */ + $api_key = config('verify.keygen_api_key', ''); + if ($api_key === '') { + return $next($data); + } + + // @codeCoverageIgnoreStart + $result = $this->token_extension->extend(); + + if (!$result->success) { + $data[] = DiagnosticData::error( + 'Keygen API token error: ' . ($result->message ?? 'unknown error') . ' Go to keygen.lycheeorg.dev to retrieve a new one.', + self::class, + ); + + return $next($data); + } + + if ($result->expires_at !== null && $result->expires_at->isBefore(now()->addWeek())) { + $data[] = DiagnosticData::warn( + 'Your Keygen API token expires on ' . $result->expires_at->toDateString() . '. Consider renewing it at keygen.lycheeorg.dev.', + self::class, + ); + } + // @codeCoverageIgnoreEnd + + return $next($data); + } +} diff --git a/app/Actions/Diagnostics/Pipes/Checks/OldLicenseCheck.php b/app/Actions/Diagnostics/Pipes/Checks/OldLicenseCheck.php index e4e45c9d78d..b882fb5729d 100644 --- a/app/Actions/Diagnostics/Pipes/Checks/OldLicenseCheck.php +++ b/app/Actions/Diagnostics/Pipes/Checks/OldLicenseCheck.php @@ -13,6 +13,7 @@ use App\Repositories\ConfigManager; use Illuminate\Support\Facades\Schema; use LycheeVerify\Contract\Status; +use LycheeVerify\Rotation; use LycheeVerify\Verify; /** @@ -22,6 +23,7 @@ class OldLicenseCheck implements DiagnosticPipe { public function __construct( private Verify $verify, + private Rotation $rotation, protected readonly ConfigManager $config_manager, ) { } @@ -47,6 +49,19 @@ public function handle(array &$data, \Closure $next): array return $next($data); } + // @codeCoverageIgnoreStart + /** @var string $api_key */ + $api_key = config('verify.keygen_api_key', ''); + if ($api_key !== '') { + $result = $this->rotation->rotate(); + if ($result->success) { + $this->verify->reset_status(); + + return $next($data); + } + } + // @codeCoverageIgnoreEnd + $data[] = DiagnosticData::error('Your license has expired. Go to keygen.lycheeorg.dev to retrieve a new one or erase the value in the license field.', self::class); return $next($data); diff --git a/app/Jobs/RotateLicenseKeyJob.php b/app/Jobs/RotateLicenseKeyJob.php new file mode 100644 index 00000000000..ad55a6a4c51 --- /dev/null +++ b/app/Jobs/RotateLicenseKeyJob.php @@ -0,0 +1,46 @@ +get_status() !== Status::FREE_EDITION) { + return; + } + + /** @var string $api_key */ + $api_key = config('verify.keygen_api_key', ''); + if ($api_key === '') { + return; + } + + // Make sure we try on login + Cache::forget(Rotation::CACHE_KEY); + $result = $rotation->rotate(); + + if ($result->success) { + $verify->reset_status(); + } + } +} diff --git a/app/Listeners/RotateLicenseKeyOnLogin.php b/app/Listeners/RotateLicenseKeyOnLogin.php new file mode 100644 index 00000000000..fe74cae13d6 --- /dev/null +++ b/app/Listeners/RotateLicenseKeyOnLogin.php @@ -0,0 +1,27 @@ +user; + + if (!$user instanceof User || $user->may_administrate !== true) { + return; + } + + RotateLicenseKeyJob::dispatch(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 17f4d1741a6..f326c89322e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -53,7 +53,9 @@ use Illuminate\Support\Str; use Laravel\Octane\Events\RequestTerminated; use Laravel\Octane\Facades\Octane; +use LycheeVerify\Contract\VerifyFactory; use LycheeVerify\Contract\VerifyInterface; +use LycheeVerify\DefaultVerifyFactory; use LycheeVerify\Verify; use Opcodes\LogViewer\Facades\LogViewer; use Safe\Exceptions\StreamException; @@ -190,6 +192,11 @@ public function register() VerifyInterface::class, Verify::class ); + + $this->app->bind( + VerifyFactory::class, + DefaultVerifyFactory::class + ); } /** diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 943325864d5..ade2268e0e4 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -34,8 +34,10 @@ use App\Listeners\RecomputeAlbumSizeOnPhotoMutation; use App\Listeners\RecomputeAlbumStatsOnAlbumChange; use App\Listeners\RecomputeAlbumStatsOnPhotoChange; +use App\Listeners\RotateLicenseKeyOnLogin; use App\Listeners\TaggedRouteCacheCleaner; use App\Listeners\WebhookListener; +use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Registered; use Illuminate\Cache\Events\CacheHit; use Illuminate\Cache\Events\CacheMissed; @@ -124,6 +126,8 @@ public function boot(): void Event::listen(AlbumSaved::class, RecomputeAlbumSizeOnAlbumChange::class . '@handleAlbumSaved'); Event::listen(AlbumDeleted::class, RecomputeAlbumSizeOnAlbumChange::class . '@handleAlbumDeleted'); + Event::listen(Login::class, RotateLicenseKeyOnLogin::class . '@handle'); + // Webhook dispatch for photo lifecycle events Event::listen(PhotoAdded::class, WebhookListener::class . '@handlePhotoAdded'); Event::listen(PhotoMoved::class, WebhookListener::class . '@handlePhotoMoved'); diff --git a/composer.lock b/composer.lock index 6caa5a21f85..441d867a21f 100644 --- a/composer.lock +++ b/composer.lock @@ -279,16 +279,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.381.2", + "version": "3.385.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "ffa8a93faafea878155853ae2caf61871363869d" + "reference": "1952d9ce58aca5a5d444f74ed1034f7ba9125002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ffa8a93faafea878155853ae2caf61871363869d", - "reference": "ffa8a93faafea878155853ae2caf61871363869d", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1952d9ce58aca5a5d444f74ed1034f7ba9125002", + "reference": "1952d9ce58aca5a5d444f74ed1034f7ba9125002", "shasum": "" }, "require": { @@ -299,7 +299,7 @@ "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/promises": "^2.0", "guzzlehttp/psr7": "^2.4.5", - "mtdowling/jmespath.php": "^2.8.0", + "mtdowling/jmespath.php": "^2.9.1", "php": ">=8.1", "psr/http-message": "^1.0 || ^2.0", "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" @@ -370,9 +370,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.381.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.385.3" }, - "time": "2026-05-15T18:08:57+00:00" + "time": "2026-06-19T18:07:45+00:00" }, { "name": "bepsvpt/secure-headers", @@ -1577,56 +1577,6 @@ ], "time": "2025-03-06T22:45:56+00:00" }, - { - "name": "entropy/entropy", - "version": "0.4.6", - "source": { - "type": "git", - "url": "https://github.com/TomasVotruba/entropy.git", - "reference": "415f69258150409db7c36aa747923e28aa53e9f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/TomasVotruba/entropy/zipball/415f69258150409db7c36aa747923e28aa53e9f6", - "reference": "415f69258150409db7c36aa747923e28aa53e9f6", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^8.4", - "webmozart/assert": "^2.4" - }, - "require-dev": { - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^12.5", - "rector/jack": "^1.0", - "rector/rector": "^2.4", - "rector/swiss-knife": "^2.4", - "shipmonk/composer-dependency-analyser": "^1.8", - "symplify/easy-coding-standard": "^13.1", - "symplify/phpstan-rules": "^14.10", - "tomasvotruba/type-coverage": "^2.2", - "tomasvotruba/unused-public": "^2.2", - "tracy/tracy": "^2.12" - }, - "type": "library", - "autoload": { - "psr-4": { - "Entropy\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "proprietary" - ], - "description": "Entropy framework", - "support": { - "issues": "https://github.com/TomasVotruba/entropy/issues", - "source": "https://github.com/TomasVotruba/entropy/tree/0.4.6" - }, - "time": "2026-06-20T10:04:38+00:00" - }, { "name": "evenement/evenement", "version": "v3.0.2", @@ -1758,16 +1708,16 @@ }, { "name": "firebase/php-jwt", - "version": "v7.0.5", + "version": "v7.1.0", "source": { "type": "git", "url": "https://github.com/googleapis/php-jwt.git", - "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", - "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0", "shasum": "" }, "require": { @@ -1776,6 +1726,7 @@ "require-dev": { "guzzlehttp/guzzle": "^7.4", "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "psr/cache": "^2.0||^3.0", @@ -1784,7 +1735,8 @@ }, "suggest": { "ext-sodium": "Support EdDSA (Ed25519) signatures", - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" }, "type": "library", "autoload": { @@ -1809,16 +1761,16 @@ } ], "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", + "homepage": "https://github.com/googleapis/php-jwt", "keywords": [ "jwt", "php" ], "support": { "issues": "https://github.com/googleapis/php-jwt/issues", - "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" + "source": "https://github.com/googleapis/php-jwt/tree/v7.1.0" }, - "time": "2026-04-01T20:38:03+00:00" + "time": "2026-06-11T17:54:14+00:00" }, { "name": "fruitcake/php-cors", @@ -3434,16 +3386,16 @@ }, { "name": "laravel/socialite", - "version": "v5.27.0", + "version": "v5.28.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e" + "reference": "4c131ff4b24d8881a9c8fe4eecb5ffeff9803f26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/40e0757a75637c7b2dff05d3286b0d8fc25e5c0e", - "reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e", + "url": "https://api.github.com/repos/laravel/socialite/zipball/4c131ff4b24d8881a9c8fe4eecb5ffeff9803f26", + "reference": "4c131ff4b24d8881a9c8fe4eecb5ffeff9803f26", "shasum": "" }, "require": { @@ -3502,7 +3454,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2026-04-24T14:05:47+00:00" + "time": "2026-06-12T03:24:05+00:00" }, { "name": "lcobucci/clock", @@ -4457,16 +4409,16 @@ }, { "name": "lychee-org/lycheeverify", - "version": "2.0.8", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/LycheeOrg/verify.git", - "reference": "be2814a6c9378a670ea6669f5ec27f91dac88338" + "reference": "7bb63166697ab01ee33e1d4ca8cc638ed24ec481" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LycheeOrg/verify/zipball/be2814a6c9378a670ea6669f5ec27f91dac88338", - "reference": "be2814a6c9378a670ea6669f5ec27f91dac88338", + "url": "https://api.github.com/repos/LycheeOrg/verify/zipball/7bb63166697ab01ee33e1d4ca8cc638ed24ec481", + "reference": "7bb63166697ab01ee33e1d4ca8cc638ed24ec481", "shasum": "" }, "require": { @@ -4535,7 +4487,7 @@ "description": "Verification package for Lychee", "homepage": "https://github.com/LycheeOrg/verify", "support": { - "source": "https://github.com/LycheeOrg/verify/tree/2.0.8", + "source": "https://github.com/LycheeOrg/verify/tree/2.1.1", "issues": "https://github.com/LycheeOrg/verify/issues" }, "funding": [ @@ -4544,7 +4496,7 @@ "url": "https://github.com/LycheeOrg" } ], - "time": "2026-06-12T15:13:59+00:00" + "time": "2026-06-21T20:43:17+00:00" }, { "name": "lychee-org/nestedset", @@ -5033,16 +4985,16 @@ }, { "name": "mtdowling/jmespath.php", - "version": "2.8.0", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + "reference": "9c208ba27ae7d90853c288b3795d6702eb251d34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", - "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9c208ba27ae7d90853c288b3795d6702eb251d34", + "reference": "9c208ba27ae7d90853c288b3795d6702eb251d34", "shasum": "" }, "require": { @@ -5051,7 +5003,7 @@ }, "require-dev": { "composer/xdebug-handler": "^3.0.3", - "phpunit/phpunit": "^8.5.33" + "phpunit/phpunit": "^8.5.52" }, "bin": [ "bin/jp.php" @@ -5059,7 +5011,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "2.9-dev" } }, "autoload": { @@ -5093,9 +5045,9 @@ ], "support": { "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + "source": "https://github.com/jmespath/jmespath.php/tree/2.9.1" }, - "time": "2024-09-04T18:46:31+00:00" + "time": "2026-06-11T10:43:56+00:00" }, { "name": "myclabs/deep-copy", @@ -5159,16 +5111,16 @@ }, { "name": "nesbot/carbon", - "version": "3.12.3", + "version": "3.13.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "6e7853a668c3107294aff38d42bf760ec02126b6" + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6e7853a668c3107294aff38d42bf760ec02126b6", - "reference": "6e7853a668c3107294aff38d42bf760ec02126b6", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", "shasum": "" }, "require": { @@ -5260,7 +5212,7 @@ "type": "tidelift" } ], - "time": "2026-06-14T20:41:42+00:00" + "time": "2026-06-18T13:49:15+00:00" }, { "name": "nette/schema", @@ -5966,16 +5918,16 @@ }, { "name": "opcodesio/mail-parser", - "version": "v0.2.3", + "version": "v0.2.4", "source": { "type": "git", "url": "https://github.com/opcodesio/mail-parser.git", - "reference": "6f9544a1e526d5849cf8d96600556b2951893aec" + "reference": "7151e4183288e46d911a76803c5545a1c8822226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opcodesio/mail-parser/zipball/6f9544a1e526d5849cf8d96600556b2951893aec", - "reference": "6f9544a1e526d5849cf8d96600556b2951893aec", + "url": "https://api.github.com/repos/opcodesio/mail-parser/zipball/7151e4183288e46d911a76803c5545a1c8822226", + "reference": "7151e4183288e46d911a76803c5545a1c8822226", "shasum": "" }, "require": { @@ -6013,9 +5965,9 @@ ], "support": { "issues": "https://github.com/opcodesio/mail-parser/issues", - "source": "https://github.com/opcodesio/mail-parser/tree/v0.2.3" + "source": "https://github.com/opcodesio/mail-parser/tree/v0.2.4" }, - "time": "2026-02-28T09:04:57+00:00" + "time": "2026-06-11T08:50:37+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -7631,20 +7583,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.2", + "version": "4.9.3", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "8429c78ca35a09f27565311b98101e2826affde0" + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", - "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8", "shasum": "" }, "require": { - "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": ">=0.8.16 <=0.18", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -7703,9 +7655,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.2" + "source": "https://github.com/ramsey/uuid/tree/4.9.3" }, - "time": "2025-12-14T04:43:48+00:00" + "time": "2026-06-18T03:57:49+00:00" }, { "name": "revolution/socialite-mastodon", @@ -8892,16 +8844,16 @@ }, { "name": "spatie/php-structure-discoverer", - "version": "2.4.2", + "version": "2.4.4", "source": { "type": "git", "url": "https://github.com/spatie/php-structure-discoverer.git", - "reference": "10cd4e0018450d23e2bd8f8472569ad0c445c0fc" + "reference": "fa2b7dae8e8a22c0306154c4b052420e054f7e2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/10cd4e0018450d23e2bd8f8472569ad0c445c0fc", - "reference": "10cd4e0018450d23e2bd8f8472569ad0c445c0fc", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/fa2b7dae8e8a22c0306154c4b052420e054f7e2b", + "reference": "fa2b7dae8e8a22c0306154c4b052420e054f7e2b", "shasum": "" }, "require": { @@ -8959,7 +8911,7 @@ ], "support": { "issues": "https://github.com/spatie/php-structure-discoverer/issues", - "source": "https://github.com/spatie/php-structure-discoverer/tree/2.4.2" + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.4.4" }, "funding": [ { @@ -8967,7 +8919,7 @@ "type": "github" } ], - "time": "2026-04-28T06:26:02+00:00" + "time": "2026-06-15T07:14:32+00:00" }, { "name": "spatie/temporary-directory", @@ -9104,16 +9056,16 @@ }, { "name": "symfony/cache", - "version": "v8.0.12", + "version": "v8.0.13", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "11dc0681506ff07ca80bfb4cbf84c601c3cf04f7" + "reference": "75f92239836ce08bce4d19f2734737dae4276fe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/11dc0681506ff07ca80bfb4cbf84c601c3cf04f7", - "reference": "11dc0681506ff07ca80bfb4cbf84c601c3cf04f7", + "url": "https://api.github.com/repos/symfony/cache/zipball/75f92239836ce08bce4d19f2734737dae4276fe9", + "reference": "75f92239836ce08bce4d19f2734737dae4276fe9", "shasum": "" }, "require": { @@ -9179,7 +9131,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v8.0.12" + "source": "https://github.com/symfony/cache/tree/v8.0.13" }, "funding": [ { @@ -9199,7 +9151,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:22:03+00:00" + "time": "2026-05-24T08:44:11+00:00" }, { "name": "symfony/cache-contracts", @@ -10355,6 +10307,89 @@ ], "time": "2026-05-23T16:22:37+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", @@ -10524,6 +10559,176 @@ ], "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", @@ -11963,75 +12168,6 @@ }, "time": "2025-12-02T11:56:42+00:00" }, - { - "name": "tomasvotruba/class-leak", - "version": "2.1.8", - "source": { - "type": "git", - "url": "https://github.com/TomasVotruba/class-leak.git", - "reference": "3ebe4d7bebfb40ab3a85a36431821367b0ac57ab" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/TomasVotruba/class-leak/zipball/3ebe4d7bebfb40ab3a85a36431821367b0ac57ab", - "reference": "3ebe4d7bebfb40ab3a85a36431821367b0ac57ab", - "shasum": "" - }, - "require": { - "entropy/entropy": "^0.4.6", - "nette/utils": "^4.1", - "nikic/php-parser": "^5.7", - "php": ">=8.4", - "symfony/finder": "^7.4|^8.0", - "webmozart/assert": "^2.0" - }, - "replace": { - "symfony/polyfill-ctype": "*", - "symfony/polyfill-intl-normalizer": "*", - "symfony/polyfill-mbstring": "*" - }, - "require-dev": { - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.2", - "phpunit/phpunit": "^13.0", - "rector/jack": "^1.0", - "rector/rector": "^2.4", - "symplify/easy-coding-standard": "^13.0", - "symplify/phpstan-extensions": "^12", - "tomasvotruba/unused-public": "^2.2", - "tracy/tracy": "^2.12" - }, - "bin": [ - "bin/class-leak", - "bin/class-leak.php" - ], - "type": "library", - "autoload": { - "psr-4": { - "TomasVotruba\\ClassLeak\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Detect leaking classes", - "support": { - "issues": "https://github.com/TomasVotruba/class-leak/issues", - "source": "https://github.com/TomasVotruba/class-leak/tree/2.1.8" - }, - "funding": [ - { - "url": "https://www.paypal.me/rectorphp", - "type": "custom" - }, - { - "url": "https://github.com/tomasvotruba", - "type": "github" - } - ], - "time": "2026-06-20T16:19:43+00:00" - }, { "name": "vlucas/phpdotenv", "version": "v5.6.3", @@ -12866,16 +13002,16 @@ }, { "name": "composer/composer", - "version": "2.9.8", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "39ee8baff8e97a1b657bbfcd6a236ff93a5efbb2" + "reference": "4120703b9bda8795075047b40361d7ec4d2abe49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/39ee8baff8e97a1b657bbfcd6a236ff93a5efbb2", - "reference": "39ee8baff8e97a1b657bbfcd6a236ff93a5efbb2", + "url": "https://api.github.com/repos/composer/composer/zipball/4120703b9bda8795075047b40361d7ec4d2abe49", + "reference": "4120703b9bda8795075047b40361d7ec4d2abe49", "shasum": "" }, "require": { @@ -12928,7 +13064,7 @@ ] }, "branch-alias": { - "dev-main": "2.9-dev" + "dev-main": "2.10-dev" } }, "autoload": { @@ -12963,7 +13099,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.9.8" + "source": "https://github.com/composer/composer/tree/2.10.1" }, "funding": [ { @@ -12975,7 +13111,7 @@ "type": "github" } ], - "time": "2026-05-13T07:28:38+00:00" + "time": "2026-06-04T08:25:59+00:00" }, { "name": "composer/metadata-minifier", @@ -13595,16 +13731,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.95.7", + "version": "v3.95.10", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "4fa4102a5617acae53f804f7c81475aaa2d6e813" + "reference": "93e1ab3e1f153024bd3ab23c8a349556063c6f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4fa4102a5617acae53f804f7c81475aaa2d6e813", - "reference": "4fa4102a5617acae53f804f7c81475aaa2d6e813", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/93e1ab3e1f153024bd3ab23c8a349556063c6f17", + "reference": "93e1ab3e1f153024bd3ab23c8a349556063c6f17", "shasum": "" }, "require": { @@ -13688,7 +13824,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.7" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.10" }, "funding": [ { @@ -13696,7 +13832,7 @@ "type": "github" } ], - "time": "2026-06-13T17:51:53+00:00" + "time": "2026-06-19T14:45:22+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -13852,16 +13988,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.8.2", + "version": "6.10.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76" + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2c89ebb95ca9cedc9347f780333f7b25792dcb76", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", "shasum": "" }, "require": { @@ -13921,9 +14057,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" }, - "time": "2026-05-05T05:39:01+00:00" + "time": "2026-06-16T20:50:26+00:00" }, { "name": "laracraft-tech/laravel-xhprof", @@ -14839,16 +14975,16 @@ }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.10", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f" + "reference": "9b000a578b85b32945b358b172c7b20e91189024" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f", - "reference": "1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024", + "reference": "9b000a578b85b32945b358b172c7b20e91189024", "shasum": "" }, "require": { @@ -14884,9 +15020,9 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.10" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11" }, - "time": "2026-02-11T14:17:32+00:00" + "time": "2026-05-02T06:54:10+00:00" }, { "name": "phpunit/php-code-coverage", @@ -15873,21 +16009,21 @@ }, { "name": "rector/rector", - "version": "2.4.5", + "version": "2.5.1", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c" + "reference": "34a9124ece04df818e6b4be4ecd0a4e23f4c0c64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/34a9124ece04df818e6b4be4ecd0a4e23f4c0c64", + "reference": "34a9124ece04df818e6b4be4ecd0a4e23f4c0c64", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.56" + "phpstan/phpstan": "^2.2.2" }, "conflict": { "rector/rector-doctrine": "*", @@ -15921,7 +16057,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.4.5" + "source": "https://github.com/rectorphp/rector/tree/2.5.1" }, "funding": [ { @@ -15929,7 +16065,7 @@ "type": "github" } ], - "time": "2026-05-26T21:03:22+00:00" + "time": "2026-06-21T10:28:27+00:00" }, { "name": "sebastian/cli-parser", @@ -16919,16 +17055,16 @@ }, { "name": "seld/jsonlint", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", "shasum": "" }, "require": { @@ -16967,7 +17103,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" }, "funding": [ { @@ -16979,7 +17115,7 @@ "type": "tidelift" } ], - "time": "2024-07-11T14:55:45+00:00" + "time": "2026-06-12T11:32:29+00:00" }, { "name": "seld/phar-utils", @@ -17092,29 +17228,29 @@ }, { "name": "slam/phpstan-extensions", - "version": "v6.8.0", + "version": "v6.9.0", "source": { "type": "git", "url": "https://github.com/Slamdunk/phpstan-extensions.git", - "reference": "772320f8e41b1fef3274cfea743987dcaa0baaa9" + "reference": "98424c7f2828169918d2d85e702d986a3422f16d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Slamdunk/phpstan-extensions/zipball/772320f8e41b1fef3274cfea743987dcaa0baaa9", - "reference": "772320f8e41b1fef3274cfea743987dcaa0baaa9", + "url": "https://api.github.com/repos/Slamdunk/phpstan-extensions/zipball/98424c7f2828169918d2d85e702d986a3422f16d", + "reference": "98424c7f2828169918d2d85e702d986a3422f16d", "shasum": "" }, "require": { "php": "~8.4.0 || ~8.5.0", - "phpstan/phpstan": "^2.1.31" + "phpstan/phpstan": "^2.1.55" }, "require-dev": { - "nette/di": "^3.2.5", - "nette/neon": "^3.4.5", - "nikic/php-parser": "^4.19.2 || ^5.6.2", - "phpstan/phpstan-phpunit": "^2.0.7", - "phpunit/phpunit": "^12.4.2", - "slam/php-cs-fixer-extensions": "^3.14.0" + "nette/di": "^3.2.6", + "nette/neon": "^3.4.8", + "nikic/php-parser": "^4.19.2 || ^5.7.0", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpunit/phpunit": "^13.1.10", + "slam/php-cs-fixer-extensions": "^3.15.0" }, "type": "phpstan-extension", "extra": { @@ -17143,7 +17279,7 @@ "description": "Slam extension of phpstan", "support": { "issues": "https://github.com/Slamdunk/phpstan-extensions/issues", - "source": "https://github.com/Slamdunk/phpstan-extensions/tree/v6.8.0" + "source": "https://github.com/Slamdunk/phpstan-extensions/tree/v6.9.0" }, "funding": [ { @@ -17155,7 +17291,7 @@ "type": "github" } ], - "time": "2025-11-01T07:13:35+00:00" + "time": "2026-05-19T03:26:23+00:00" }, { "name": "staabm/side-effects-detector", @@ -17578,29 +17714,47 @@ }, { "name": "symplify/phpstan-rules", - "version": "14.9.11", + "version": "14.12.0", "source": { "type": "git", "url": "https://github.com/symplify/phpstan-rules.git", - "reference": "5ea4bbd9357cba253aada506dd96d37d7069ac3b" + "reference": "31242401e8498c9418a14189e48b2b53158d3f7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symplify/phpstan-rules/zipball/5ea4bbd9357cba253aada506dd96d37d7069ac3b", - "reference": "5ea4bbd9357cba253aada506dd96d37d7069ac3b", + "url": "https://api.github.com/repos/symplify/phpstan-rules/zipball/31242401e8498c9418a14189e48b2b53158d3f7b", + "reference": "31242401e8498c9418a14189e48b2b53158d3f7b", "shasum": "" }, "require": { - "nette/utils": "^3.2|^4.0", - "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.33", - "webmozart/assert": "^1.12 || ^2.0" + "nette/utils": "^4.1", + "php": "^8.4", + "phpstan/phpdoc-parser": "^2.3", + "phpstan/phpstan": "^2.2", + "webmozart/assert": "^2.4" + }, + "require-dev": { + "illuminate/container": "^11.51", + "nikic/php-parser": "^5.7", + "phpstan/extension-installer": "^1.4", + "phpunit/phpunit": "^13.2", + "rector/jack": "^1.0", + "rector/rector": "^2.4", + "shipmonk/composer-dependency-analyser": "^1.8", + "symfony/framework-bundle": "^6.4", + "symplify/easy-coding-standard": "^13.2", + "tomasvotruba/class-leak": "^2.1", + "tomasvotruba/type-coverage": "^2.2", + "tomasvotruba/unused-public": "^2.2" }, "type": "phpstan-extension", "extra": { "phpstan": { "includes": [ - "config/services/services.neon" + "config/services/services.neon", + "config/ctor-rules.neon", + "config/mock-rules.neon", + "config/phpstan-extensions.neon" ] } }, @@ -17616,10 +17770,10 @@ "license": [ "MIT" ], - "description": "Set of Symplify rules for PHPStan", + "description": "Set of Symplify rules, type extensions and error formatter for PHPStan", "support": { "issues": "https://github.com/symplify/phpstan-rules/issues", - "source": "https://github.com/symplify/phpstan-rules/tree/14.9.11" + "source": "https://github.com/symplify/phpstan-rules/tree/14.12.0" }, "funding": [ { @@ -17631,27 +17785,27 @@ "type": "github" } ], - "time": "2026-01-05T13:53:59+00:00" + "time": "2026-06-15T08:34:08+00:00" }, { "name": "thecodingmachine/phpstan-safe-rule", - "version": "v1.4.3", + "version": "v1.4.4", "source": { "type": "git", "url": "https://github.com/thecodingmachine/phpstan-safe-rule.git", - "reference": "5c804889253ce9498ef185e108e9f94b6023208e" + "reference": "93a9f4db9f77dc25d0ffa36b2131ba3fc4599516" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/5c804889253ce9498ef185e108e9f94b6023208e", - "reference": "5c804889253ce9498ef185e108e9f94b6023208e", + "url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/93a9f4db9f77dc25d0ffa36b2131ba3fc4599516", + "reference": "93a9f4db9f77dc25d0ffa36b2131ba3fc4599516", "shasum": "" }, "require": { "nikic/php-parser": "^5", "php": "^8.1", - "phpstan/phpstan": "^2.1.11", - "thecodingmachine/safe": "^1.2 || ^2.0 || ^3.0" + "phpstan/phpstan": "^2.1.30", + "thecodingmachine/safe": "^3.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.1", @@ -17687,9 +17841,9 @@ "description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe", "support": { "issues": "https://github.com/thecodingmachine/phpstan-safe-rule/issues", - "source": "https://github.com/thecodingmachine/phpstan-safe-rule/tree/v1.4.3" + "source": "https://github.com/thecodingmachine/phpstan-safe-rule/tree/v1.4.4" }, - "time": "2025-11-21T09:41:49+00:00" + "time": "2026-06-21T07:27:08+00:00" }, { "name": "theseer/tokenizer", @@ -17740,6 +17894,54 @@ } ], "time": "2025-11-17T20:03:58+00:00" + }, + { + "name": "tomasvotruba/class-leak", + "version": "2.1.4", + "source": { + "type": "git", + "url": "https://github.com/TomasVotruba/class-leak.git", + "reference": "c13a50f0d4593d17fb19a2278a59556662e663af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TomasVotruba/class-leak/zipball/c13a50f0d4593d17fb19a2278a59556662e663af", + "reference": "c13a50f0d4593d17fb19a2278a59556662e663af", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "bin": [ + "bin/class-leak", + "bin/class-leak.php" + ], + "type": "library", + "autoload": { + "psr-4": { + "TomasVotruba\\ClassLeak\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect leaking classes", + "support": { + "issues": "https://github.com/TomasVotruba/class-leak/issues", + "source": "https://github.com/TomasVotruba/class-leak/tree/2.1.4" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-05-27T08:33:55+00:00" } ], "aliases": [], @@ -17771,5 +17973,5 @@ "platform-overrides": { "php": "8.4" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/docker-compose.yaml b/docker-compose.yaml index 06fadf5c550..7f22bbfc12f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -185,6 +185,17 @@ x-common-env: # Or you can uncomment the following line to use DB_PASSWORD_FILE from secrets # DB_PASSWORD_FILE: "/run/secrets/db_password" + ################################################################### + # Keygen License Management # + ################################################################### + + # API token obtained from keygen.lycheeorg.dev. + # When set, Lychee will automatically rotate an expired license key + # on admin login and check the token health on the diagnostics page. + KEYGEN_API_KEY: "${KEYGEN_API_KEY:-}" + # Or you can uncomment the following line to use KEYGEN_API_KEY_FILE from secrets + # KEYGEN_API_KEY_FILE: "/run/secrets/keygen_api_key" + # Session configuration SESSION_DRIVER: "${SESSION_DRIVER:-file}" SESSION_LIFETIME: "${SESSION_LIFETIME:-120}" diff --git a/docker/scripts/01-validate-env.sh b/docker/scripts/01-validate-env.sh index 9f4d07f313f..ec68757768f 100644 --- a/docker/scripts/01-validate-env.sh +++ b/docker/scripts/01-validate-env.sh @@ -138,6 +138,29 @@ if [ -n "${REDIS_HOST:-}" ] || [ -n "${REDIS_PASSWORD:-}" ] || [ -n "${REDIS_PAS fi fi +########################### +# VALIDATE KEYGEN KEY # +########################### + +# Check if KEYGEN_API_KEY exists, with fallback mechanisms +if [ -z "${KEYGEN_API_KEY:-}" ]; then + # Check if KEYGEN_API_KEY_FILE is set and load from file + if [ -n "${KEYGEN_API_KEY_FILE:-}" ]; then + if [ -f "$KEYGEN_API_KEY_FILE" ]; then + KEYGEN_API_KEY=$(cat "$KEYGEN_API_KEY_FILE") + export KEYGEN_API_KEY + echo "✅ Loaded KEYGEN_API_KEY from file: ${KEYGEN_API_KEY_FILE}" + else + echo "❌ ERROR: KEYGEN_API_KEY_FILE is set but file does not exist: ${KEYGEN_API_KEY_FILE}" + exit 1 + fi + # Fallback to /app/.env if it exists + elif [ -f "/app/.env" ]; then + KEYGEN_API_KEY=$(grep "^KEYGEN_API_KEY=" /app/.env | cut -d= -f2- | tr -d '"' | tr -d "'") + export KEYGEN_API_KEY + fi +fi + ########################### # ADDITIONAL ENV # ########################### diff --git a/docs/specs/2-how-to/keygen-license-management.md b/docs/specs/2-how-to/keygen-license-management.md new file mode 100644 index 00000000000..20a4bb53b18 --- /dev/null +++ b/docs/specs/2-how-to/keygen-license-management.md @@ -0,0 +1,62 @@ +# Keygen License Management + +This guide explains how to configure automatic license key rotation and API token monitoring for Lychee Supporter/Pro editions. + +## Overview + +Lychee uses license keys to unlock Supporter and Pro features. These keys can expire over time. When a **Keygen API token** is configured, Lychee provides two automatic safeguards: + +- **Automatic key rotation**: when an admin logs in and the current license key is expired, Lychee attempts to fetch a fresh key from the Keygen server in the background (after the response is sent). +- **Token health check**: the diagnostics page warns administrators if their Keygen API token is expired or about to expire. + +## Requirements + +- A valid account at [keygen.lycheeorg.dev](https://keygen.lycheeorg.dev) +- An API token generated from your Keygen account + +## Configuration + +Add the following to your `.env` file: + +```env +KEYGEN_API_KEY= +``` + +### Docker + +Pass the variable through your `docker-compose.yaml`: + +```yaml +environment: + - KEYGEN_API_KEY=${KEYGEN_API_KEY} +``` + +## How It Works + +### Automatic License Rotation + +1. An admin logs in (via any method: local, LDAP, OAuth, WebAuthn). +2. After the response is sent, a background job checks whether the current license key is expired. +3. If the key is expired and `KEYGEN_API_KEY` is set, Lychee calls the Keygen API to fetch a new license key. +4. On success the new key is stored in the database and takes effect immediately. +5. On failure a 24-hour cooldown prevents repeated API calls. + +The same rotation is also attempted when visiting the diagnostics page with an expired license. + +### Diagnostics Token Check + +When an admin visits the diagnostics page, Lychee extends the Keygen API token and inspects its expiration date: + +- **Error** — the token is invalid or the API returned an error. The token should be regenerated at [keygen.lycheeorg.dev](https://keygen.lycheeorg.dev). +- **Warning** — the token expires within one week. Consider renewing it. + +This check is only visible to administrators. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| "Your license has expired" on diagnostics | Key expired and no `KEYGEN_API_KEY` set | Set `KEYGEN_API_KEY` in `.env`, or manually retrieve a new key from [keygen.lycheeorg.dev](https://keygen.lycheeorg.dev) | +| "Keygen API token error: …" on diagnostics | API token is invalid or revoked | Generate a new token at [keygen.lycheeorg.dev](https://keygen.lycheeorg.dev) and update `KEYGEN_API_KEY` | +| "Retry timeout active" in logs | A previous rotation attempt failed | Wait 24 hours for the cooldown to expire, or clear the cache (`php artisan cache:forget verify.rotation.next_retry`) | +| Rotation never triggers | User logging in is not an admin | Only admin logins (`may_administrate = true`) trigger rotation |