From 6483087a806b798ba663d3a762294320ba2481ef Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:42:30 +0000 Subject: [PATCH 01/36] Cover atomic configuration, event and route cache publication Reconcile Laravel framework PR 62117 against the frozen 13.x source at 5d7797528723107cd4f495326838a7d7d67fbb20. The cache commands already publish through Filesystem::replace, so port the missing assertions that cache creation uses atomic replacement and does not write in place. Consolidate redundant event and route success cases while retaining success output and file-existence assertions. Preserve the isolated config and route subprocess fixtures and the nullable replacement mode argument. All three complete test files passed before review; these reviewed changes were held intact for this batch to keep the preceding initial PR within its file limit. --- .../Foundation/Console/ConfigCacheCommandTest.php | 13 +++++++++++++ .../Foundation/Console/EventCacheCommandTest.php | 13 ++++++------- .../Foundation/Console/RouteCacheCommandTest.php | 8 +++++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/tests/Integration/Foundation/Console/ConfigCacheCommandTest.php b/tests/Integration/Foundation/Console/ConfigCacheCommandTest.php index 5a79c52676..cf27481958 100644 --- a/tests/Integration/Foundation/Console/ConfigCacheCommandTest.php +++ b/tests/Integration/Foundation/Console/ConfigCacheCommandTest.php @@ -65,6 +65,19 @@ public function testConfigurationCanBeCachedSuccessfully(): void $this->assertFileExists($this->app->getCachedConfigPath()); } + public function testConfigurationIsCachedAtomically(): void + { + $files = m::mock(Filesystem::class)->makePartial(); + $files->shouldReceive('replace')->once()->with($this->app->getCachedConfigPath(), m::type('string'), null)->passthru(); + $files->shouldNotReceive('put'); + + $this->app->instance('files', $files); + + $this->artisan('config:cache')->assertSuccessful(); + + $this->assertFileExists($this->app->getCachedConfigPath()); + } + public function testConfigurationCacheFailsWithNonSerializableValue(): void { $files = new Filesystem; diff --git a/tests/Integration/Foundation/Console/EventCacheCommandTest.php b/tests/Integration/Foundation/Console/EventCacheCommandTest.php index 4e9e27cfe7..d036408e42 100644 --- a/tests/Integration/Foundation/Console/EventCacheCommandTest.php +++ b/tests/Integration/Foundation/Console/EventCacheCommandTest.php @@ -19,19 +19,18 @@ protected function tearDown(): void parent::tearDown(); } - public function testEventsCacheCommandCreatesFile(): void + public function testEventsAreCachedAtomically(): void { - $this->artisan('event:cache') - ->assertSuccessful(); + $files = m::mock(Filesystem::class)->makePartial(); + $files->shouldReceive('replace')->once()->with($this->app->getCachedEventsPath(), m::type('string'), null)->passthru(); - $this->assertFileExists($this->app->getCachedEventsPath()); - } + $this->app->instance('files', $files); - public function testEventsCacheCommandOutputsSuccessMessage(): void - { $this->artisan('event:cache') ->expectsOutputToContain('Events cached successfully.') ->assertSuccessful(); + + $this->assertFileExists($this->app->getCachedEventsPath()); } public function testCachedFileContainsProviderEvents(): void diff --git a/tests/Integration/Foundation/Console/RouteCacheCommandTest.php b/tests/Integration/Foundation/Console/RouteCacheCommandTest.php index 2dbdb64136..603324beca 100644 --- a/tests/Integration/Foundation/Console/RouteCacheCommandTest.php +++ b/tests/Integration/Foundation/Console/RouteCacheCommandTest.php @@ -72,7 +72,7 @@ protected function tearDown(): void CleanupActions::run(...$actions); } - public function testRouteCacheSucceedsWithSourceRoutes(): void + public function testRoutesAreCachedAtomically(): void { $this->defineTestbenchRoutes( <<<'PHP' @@ -80,6 +80,12 @@ public function testRouteCacheSucceedsWithSourceRoutes(): void PHP ); + $files = m::mock(Filesystem::class)->makePartial(); + $files->shouldReceive('replace')->once()->with($this->app->getCachedRoutesPath(), m::type('string'), null)->passthru(); + $files->shouldNotReceive('put'); + + $this->app->instance('files', $files); + $this->artisan('route:cache') ->assertSuccessful() ->expectsOutputToContain('Routes cached successfully'); From 5ac328cdbf084bd40a501e1bb7c1e886b28df929 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:29:23 +0000 Subject: [PATCH 02/36] Preserve database transactions when acquiring existing cache locks Port laravel/framework#62114 from 2184bbe43573547e5ecb1c5ab8527449e91bb61b, reconciled against the frozen 13.x source. Use conflict-safe insertion so a duplicate lock does not abort a PostgreSQL transaction before the conditional takeover update. Keep strict inserts on MySQL and MariaDB: INSERT IGNORE can truncate an oversized lock name and leave a lock that its original name cannot release. Preserve the SQLite read-first refusal, unique-violation-only takeover and existing ownership verification. Port both transaction regressions and add an oversized-name regression that fails under INSERT IGNORE. Verification: complete unit tests and integration tests on SQLite, PostgreSQL 14, MySQL 8.4 and MariaDB 10.11; static analysis and formatting pass. --- src/cache/src/DatabaseLock.php | 26 +++++++---- tests/Cache/CacheDatabaseLockTest.php | 30 ++++++------- .../Integration/Database/DatabaseLockTest.php | 44 +++++++++++++++++++ 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/cache/src/DatabaseLock.php b/src/cache/src/DatabaseLock.php index c090c35b34..0cf55f7f4b 100644 --- a/src/cache/src/DatabaseLock.php +++ b/src/cache/src/DatabaseLock.php @@ -83,10 +83,11 @@ protected function connection(): ConnectionInterface public function acquire(): bool { $connection = $this->connection(); + $driver = $connection->getDriverName(); - // Every SQLite write takes the database-wide write lock, even an insert that fails on the existing key or an + // Every SQLite write takes the database-wide write lock, even an insert that ignores the existing key or an // update that matches nothing, so a live lock held by another owner is refused with a read instead. - if ($connection->getDriverName() === 'sqlite') { + if ($driver === 'sqlite') { $owner = $connection->table($this->table) ->useWritePdo() ->where('key', $this->name) @@ -98,16 +99,23 @@ public function acquire(): bool } } - try { - $connection->table($this->table)->insert([ - 'key' => $this->name, - 'owner' => $this->owner, - 'expiration' => $this->expiresAt(), - ]); + $record = [ + 'key' => $this->name, + 'owner' => $this->owner, + 'expiration' => $this->expiresAt(), + ]; - $acquired = true; + try { + // MySQL's INSERT IGNORE truncates overlong names, leaving locks that cannot be released by their full name. + $acquired = in_array($driver, ['mysql', 'mariadb'], true) + ? $connection->table($this->table)->insert($record) + : $connection->table($this->table)->insertOrIgnore($record) > 0; } catch (UniqueConstraintViolationException) { // Only an existing row means the lock may still be taken over; other failures, such as a busy database, propagate. + $acquired = false; + } + + if (! $acquired) { $updated = $connection->table($this->table) ->where('key', $this->name) ->where(function ($query) { diff --git a/tests/Cache/CacheDatabaseLockTest.php b/tests/Cache/CacheDatabaseLockTest.php index cdc2709d5b..8f6ddad25b 100644 --- a/tests/Cache/CacheDatabaseLockTest.php +++ b/tests/Cache/CacheDatabaseLockTest.php @@ -28,12 +28,12 @@ public function testLockCanBeAcquired(): void { [$lock, $table] = $this->getLock(); - $table->shouldReceive('insert')->once()->with(m::on(function ($arg) { + $table->shouldReceive('insertOrIgnore')->once()->with(m::on(function ($arg) { return is_array($arg) && $arg['key'] === 'foo' && isset($arg['owner']) && is_int($arg['expiration']); - }))->andReturn(true); + }))->andReturn(1); $this->assertTrue($lock->acquire()); } @@ -65,11 +65,11 @@ public function testFractionalSecondAcquireAndRefreshNeverExpireBeforeRequestedD [$lock, $table] = $this->getLock(seconds: 1); $owner = $lock->owner(); - $table->shouldReceive('insert')->once()->with([ + $table->shouldReceive('insertOrIgnore')->once()->with([ 'key' => 'foo', 'owner' => $owner, 'expiration' => 1002, - ])->andReturn(true); + ])->andReturn(1); $this->assertTrue($lock->acquire()); @@ -86,8 +86,8 @@ public function testLockCanBeAcquiredIfAlreadyOwnedBySameOwner(): void [$lock, $table] = $this->getLock(); $owner = $lock->owner(); - // First attempt throws exception (key exists) - $table->shouldReceive('insert')->once()->andThrow(new UniqueConstraintViolationException('', '', [], new Exception)); + // First attempt inserts nothing because the key exists. + $table->shouldReceive('insertOrIgnore')->once()->andReturn(0); // So it tries to update $table->shouldReceive('where')->once()->with('key', 'foo')->andReturn($table); @@ -111,8 +111,8 @@ public function testLockCannotBeAcquiredIfAlreadyHeld(): void { [$lock, $table] = $this->getLock(); - // Insert fails - $table->shouldReceive('insert')->once()->andThrow(new UniqueConstraintViolationException('', '', [], new Exception)); + // Insert finds the existing key. + $table->shouldReceive('insertOrIgnore')->once()->andReturn(0); // Update fails too (someone else owns it) $table->shouldReceive('where')->once()->with('key', 'foo')->andReturn($table); @@ -139,7 +139,7 @@ public function testInsertFailureOtherThanAnExistingLockPropagatesWithoutTakingO [$lock, $table] = $this->getLock(); $failure = new QueryException('default', 'insert into "cache_locks"', [], new PDOException('SQLSTATE[HY000]: General error: 5 database is locked')); - $table->shouldReceive('insert')->once()->andThrow($failure); + $table->shouldReceive('insertOrIgnore')->once()->andThrow($failure); $table->shouldNotReceive('update'); try { @@ -161,7 +161,7 @@ public function testSqliteLockHeldByAnotherOwnerIsRefusedWithoutWriting(): void public function testSqliteLockIsInsertedWhenNoLiveLockExists(): void { $insert = m::mock(Builder::class); - $insert->shouldReceive('insert')->once()->andReturn(true); + $insert->shouldReceive('insertOrIgnore')->once()->andReturn(1); [$lock] = $this->getLock(driver: 'sqlite', tables: [$this->liveOwnerRead(static fn (): null => null), $insert]); @@ -178,7 +178,7 @@ public function testSqliteExpiredOrOwnLockIsTakenOverByTheConditionalUpdate(bool return $own ? $lock->owner() : null; }); $insert = m::mock(Builder::class); - $insert->shouldReceive('insert')->once()->andThrow(new UniqueConstraintViolationException('', '', [], new Exception)); + $insert->shouldReceive('insertOrIgnore')->once()->andReturn(0); $update = m::mock(Builder::class); $update->shouldReceive('where')->once()->with('key', 'foo')->andReturnSelf(); $update->shouldReceive('where')->once()->with(m::type(Closure::class))->andReturnSelf(); @@ -230,7 +230,7 @@ public function testExpiredLocksAreDeletedDuringAcquisition(): void { [$lock, $table] = $this->getLock(lockLottery: [1, 1]); // Always hit lottery - $table->shouldReceive('insert')->once()->andReturn(true); + $table->shouldReceive('insertOrIgnore')->once()->andReturn(1); // Lottery cleanup $table->shouldReceive('where')->once()->with('expiration', '<=', m::type('int'))->andReturn($table); @@ -245,7 +245,7 @@ public function testLockCanBeAcquiredWithoutAutomaticPruning(?array $lottery): v { [$lock, $table] = $this->getLock(lockLottery: $lottery); - $table->shouldReceive('insert')->once()->andReturn(true); + $table->shouldReceive('insertOrIgnore')->once()->andReturn(1); $this->assertTrue($lock->acquire()); } @@ -302,12 +302,12 @@ public function testLockWithDefaultTimeout(): void [$lock, $table] = $this->getLock(seconds: 0); - $table->shouldReceive('insert')->once()->with(m::on(function ($arg) use ($now) { + $table->shouldReceive('insertOrIgnore')->once()->with(m::on(function ($arg) use ($now) { return is_array($arg) && $arg['key'] === 'foo' && isset($arg['owner']) && $arg['expiration'] === $now->getTimestamp() + 86400; // Default timeout - }))->andReturn(true); + }))->andReturn(1); $this->assertTrue($lock->acquire()); } diff --git a/tests/Integration/Database/DatabaseLockTest.php b/tests/Integration/Database/DatabaseLockTest.php index 8978eea124..dc7eb1e48f 100644 --- a/tests/Integration/Database/DatabaseLockTest.php +++ b/tests/Integration/Database/DatabaseLockTest.php @@ -46,6 +46,22 @@ public function testLockCanBeAcquired(): void $otherLock->release(); } + public function testOverlongLockNamesFailWithoutLeavingATruncatedLock(): void + { + if (! in_array($this->driver, ['mysql', 'mariadb'], true)) { + $this->markTestSkipped('Only MySQL and MariaDB truncate overlong names with INSERT IGNORE.'); + } + + try { + Cache::store('database')->lock(str_repeat('a', 300))->get(); + $this->fail('The overlong lock name was accepted.'); + } catch (QueryException $exception) { + $this->assertSame('22001', $exception->getCode()); + } + + $this->assertSame(0, DB::table('cache_locks')->count()); + } + public function testOwnerCanAcquireItsLiveLockAgainWithinTheSameSecond(): void { // A frozen clock gives the second acquisition the expiration the lock already has, so its update changes nothing. @@ -132,6 +148,34 @@ public function testExpiredLockCanBeRetrieved(): void $otherLock->release(); } + public function testLockCannotBeAcquiredWhileHeldInsideTransaction(): void + { + $lock = Cache::driver('database')->lock('foo'); + $this->assertTrue($lock->get()); + + DB::transaction(function (): void { + $this->assertFalse(Cache::driver('database')->lock('foo')->get()); + + $this->assertSame(1, DB::table('cache_locks')->count()); + }); + + $lock->release(); + } + + public function testExpiredLockCanBeAcquiredInsideTransaction(): void + { + $lock = Cache::driver('database')->lock('foo'); + $this->assertTrue($lock->get()); + DB::table('cache_locks')->update(['expiration' => CarbonImmutable::now()->subDay()->getTimestamp()]); + + DB::transaction(function (): void { + $otherLock = Cache::driver('database')->lock('foo'); + $this->assertTrue($otherLock->get()); + + $otherLock->release(); + }); + } + public function testIsLocked(): void { $lock = Cache::driver('database')->lock('foo'); From ebbe8d38ec4b0612b9dda4c3f05a658125b9a497 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:29:37 +0000 Subject: [PATCH 03/36] Retry unrestorable jobs and distinguish string-job data from object metadata Port laravel/framework#62124 from 6f3025721c3b5643649289f3cc54098a65374974, reconciled against the frozen 13.x source. Missing models no longer interrupt retrying subsequent jobs. Missing models, removed classes and string jobs retain the SQS driver defaults required by FIFO queues, including pooled connections. Mark string payloads before payload hooks and share their classification through JobName. Caller-owned command, commandName and batchId fields no longer trigger object deserialization, batch cleanup, misleading failed-job names or Telescope failures. Unmarked Laravel object payloads and custom createObjectPayload handlers retain their existing format and behavior; no handler allowlist or worker state is introduced. Normalize non-array string data only when Telescope captures it, retaining its protected array signatures and leaving dispatched data unchanged. Keep the conservative Sentry exclusion for opaque serialized values. Unmarked imported string payloads retain the upstream ambiguity because their format cannot distinguish caller data from custom object metadata. Verification: both upstream missing-model regressions, real-produced string payloads, custom-handler retryUntil, standard/FIFO options, timeout cleanup, failed-job listing and Telescope recording. The full queue suite passed before review trimming; all affected complete files pass after corrections. Full static analysis, targeted correction analysis and formatting pass. --- src/queue/src/Console/ListFailedCommand.php | 3 +- src/queue/src/Console/RetryCommand.php | 25 +++- src/queue/src/Jobs/Job.php | 3 +- src/queue/src/Jobs/JobName.php | 10 ++ src/queue/src/Queue.php | 2 + src/telescope/src/Watchers/JobWatcher.php | 18 ++- tests/Queue/QueueBeanstalkdQueueTest.php | 4 +- tests/Queue/QueueDatabaseQueueUnitTest.php | 6 +- tests/Queue/QueueJobTest.php | 46 +++++- tests/Queue/QueueListFailedCommandTest.php | 19 +++ tests/Queue/QueueRedisQueueTest.php | 20 +-- tests/Queue/QueueRetryCommandTest.php | 156 +++++++++++++++++++- tests/Telescope/Watchers/JobWatcherTest.php | 46 ++++++ 13 files changed, 327 insertions(+), 31 deletions(-) diff --git a/src/queue/src/Console/ListFailedCommand.php b/src/queue/src/Console/ListFailedCommand.php index be77eaf758..05abe54f61 100644 --- a/src/queue/src/Console/ListFailedCommand.php +++ b/src/queue/src/Console/ListFailedCommand.php @@ -6,6 +6,7 @@ use Hypervel\Console\Command; use Hypervel\Queue\Failed\FailedJobProviderInterface; +use Hypervel\Queue\Jobs\JobName; use Hypervel\Support\Arr; use Hypervel\Support\Collection; use Symfony\Component\Console\Attribute\AsCommand; @@ -93,7 +94,7 @@ private function extractJobName(string $payload): ?string return null; } - if (! isset($payload['data']['command'])) { + if (! JobName::hasJobObject($payload)) { return is_string($payload['job'] ?? null) ? $payload['job'] : null; } diff --git a/src/queue/src/Console/RetryCommand.php b/src/queue/src/Console/RetryCommand.php index ba9eac7d42..bc44564bda 100644 --- a/src/queue/src/Console/RetryCommand.php +++ b/src/queue/src/Console/RetryCommand.php @@ -11,8 +11,10 @@ use Hypervel\Contracts\Encryption\Encrypter; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Queue\Queue; +use Hypervel\Database\Eloquent\ModelNotFoundException; use Hypervel\Queue\Events\JobRetryRequested; use Hypervel\Queue\Failed\FailedJobProviderInterface; +use Hypervel\Queue\Jobs\JobName; use Hypervel\Queue\QueuePoolProxy; use Hypervel\Queue\SqsQueue; use Hypervel\Support\Enumerable; @@ -188,11 +190,15 @@ protected function refreshRetryUntil(string $payload): string { $payload = json_decode($payload, true, flags: JSON_THROW_ON_ERROR); - if (! isset($payload['data']['command'])) { + if (! JobName::hasJobObject($payload)) { return json_encode($payload, JSON_THROW_ON_ERROR); } - $instance = $this->getInstanceFromPayload($payload); + try { + $instance = $this->getInstanceFromPayload($payload); + } catch (ModelNotFoundException) { + return json_encode($payload, JSON_THROW_ON_ERROR); + } if (is_object($instance) && ! $instance instanceof __PHP_Incomplete_Class && method_exists($instance, 'retryUntil')) { $retryUntil = $instance->retryUntil(); @@ -218,12 +224,21 @@ protected function getQueueableOptions(Queue $queue, stdClass $job): array $payload = json_decode($job->payload, true, flags: JSON_THROW_ON_ERROR); - if (! isset($payload['data']['command'])) { - return []; + try { + $instance = JobName::hasJobObject($payload) + ? $this->getInstanceFromPayload($payload) + : $payload['job']; + } catch (ModelNotFoundException) { + $instance = null; + } + + // Unrestorable jobs still need the driver's string-job defaults, including required FIFO message options. + if ($instance === null || $instance instanceof __PHP_Incomplete_Class) { + $instance = $payload['data']['commandName']; } $getOptions = fn (SqsQueue $queue) => $queue->getQueueableOptions( - $this->getInstanceFromPayload($payload), + $instance, $job->queue, $job->payload, ); diff --git a/src/queue/src/Jobs/Job.php b/src/queue/src/Jobs/Job.php index e6c5b3bac2..d87e234ad4 100644 --- a/src/queue/src/Jobs/Job.php +++ b/src/queue/src/Jobs/Job.php @@ -308,7 +308,8 @@ public function fail(?Throwable $e = null): void $this->markAsFailed(); try { - $commandName = $this->payload()['data']['commandName'] ?? false; + $payload = $this->payload(); + $commandName = JobName::hasJobObject($payload) ? ($payload['data']['commandName'] ?? false) : false; } catch (InvalidPayloadException) { $commandName = false; } diff --git a/src/queue/src/Jobs/JobName.php b/src/queue/src/Jobs/JobName.php index 8680e53a58..237aca720d 100644 --- a/src/queue/src/Jobs/JobName.php +++ b/src/queue/src/Jobs/JobName.php @@ -16,6 +16,16 @@ public static function parse(string $job): array return Str::parseCallback($job, 'fire'); } + /** + * Determine whether the payload carries an object job. + */ + public static function hasJobObject(array $payload): bool + { + // Check the marker before caller-owned data, which may still be an object in payload hooks. + // Unmarked payloads retain Laravel's format, including custom object-job handlers. + return ! isset($payload['stringJob']) && isset($payload['data']['command']); + } + /** * Get the resolved name of the queued job class. */ diff --git a/src/queue/src/Queue.php b/src/queue/src/Queue.php index 6a99b23852..44a1db8309 100644 --- a/src/queue/src/Queue.php +++ b/src/queue/src/Queue.php @@ -379,6 +379,8 @@ protected function createStringPayload(array|string $job, ?string $queue, mixed 'uuid' => (string) Str::uuid(), 'displayName' => is_string($job) ? explode('@', $job)[0] : null, 'job' => $job, + // Caller data must not be interpreted as serialized object-job metadata. + 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, diff --git a/src/telescope/src/Watchers/JobWatcher.php b/src/telescope/src/Watchers/JobWatcher.php index 9801176fc8..e6fffa7200 100644 --- a/src/telescope/src/Watchers/JobWatcher.php +++ b/src/telescope/src/Watchers/JobWatcher.php @@ -13,6 +13,7 @@ use Hypervel\Queue\Events\JobFailed; use Hypervel\Queue\Events\JobProcessed; use Hypervel\Queue\InvalidPayloadException; +use Hypervel\Queue\Jobs\JobName; use Hypervel\Queue\Queue; use Hypervel\Support\Arr; use Hypervel\Support\Str; @@ -22,6 +23,7 @@ use Hypervel\Telescope\ExtractProperties; use Hypervel\Telescope\ExtractTags; use Hypervel\Telescope\IncomingEntry; +use Hypervel\Telescope\JsonNormalizer; use Hypervel\Telescope\Telescope; use RuntimeException; @@ -60,7 +62,8 @@ public function recordJob(string $connection, ?string $queue, array $payload): ? return null; } - $job = isset($payload['data']['command']) + $hasJobObject = JobName::hasJobObject($payload); + $job = $hasJobObject ? get_class($payload['data']['command']) : $payload['job']; @@ -78,7 +81,7 @@ public function recordJob(string $connection, ?string $queue, array $payload): ? Telescope::recordJob( $entry = IncomingEntry::make($content) - ->withFamilyHash($content['data']['batchId'] ?? null) + ->withFamilyHash($hasJobObject ? ($content['data']['batchId'] ?? null) : null) ->tags($this->tags($payload)) ); @@ -207,8 +210,11 @@ protected function defaultJobData(string $connection, ?string $queue, array $pay */ protected function data(array $payload): array { - if (! isset($payload['data']['command'])) { - return $payload['data']; + if (! JobName::hasJobObject($payload)) { + // String handlers accept arbitrary JSON data; the entry's display contract requires an array. + return is_array($payload['data']) + ? $payload['data'] + : Arr::wrap(JsonNormalizer::normalize($payload['data'])); } return ExtractProperties::from( @@ -221,7 +227,7 @@ protected function data(array $payload): array */ protected function tags(array $payload): array { - if (! isset($payload['data']['command'])) { + if (! JobName::hasJobObject($payload)) { return []; } @@ -235,7 +241,7 @@ protected function tags(array $payload): array */ protected function updateBatch(array $payload): void { - if (! isset($payload['data']['command'])) { + if (! JobName::hasJobObject($payload)) { return; } diff --git a/tests/Queue/QueueBeanstalkdQueueTest.php b/tests/Queue/QueueBeanstalkdQueueTest.php index 4fce4c86bd..bbcc36f233 100644 --- a/tests/Queue/QueueBeanstalkdQueueTest.php +++ b/tests/Queue/QueueBeanstalkdQueueTest.php @@ -199,7 +199,7 @@ public function testPushProperlyPushesJobOntoBeanstalkd(): void $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->expects('useTube')->with(m::type(TubeName::class)); $pheanstalk->expects('useTube')->with(m::type(TubeName::class)); - $pheanstalk->expects('put')->times(2)->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => null]), 1024, 0, 60); + $pheanstalk->expects('put')->times(2)->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => null]), 1024, 0, 60); $this->queue->push('foo', ['data'], 'stack'); $this->queue->push('foo', ['data']); @@ -245,7 +245,7 @@ public function testDelayedPushProperlyPushesJobOntoBeanstalkd(): void $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->expects('useTube')->with(m::type(TubeName::class)); $pheanstalk->expects('useTube')->with(m::type(TubeName::class)); - $pheanstalk->expects('put')->times(2)->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => 5]), Pheanstalk::DEFAULT_PRIORITY, 5, Pheanstalk::DEFAULT_TTR); + $pheanstalk->expects('put')->times(2)->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => 5]), Pheanstalk::DEFAULT_PRIORITY, 5, Pheanstalk::DEFAULT_TTR); $this->queue->later(5, 'foo', ['data'], 'stack'); $this->queue->later(5, 'foo', ['data']); diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 1d565ef4b9..dba089bbe6 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -265,7 +265,7 @@ public function testDelayedPushProperlyPushesJobOntoDatabase(DateInterval|DateTi $query->expects('insertGetId')->andReturnUsing(function (array $array) use ($uuid, $now): int { $this->assertSame('default', $array['queue']); - $this->assertSame(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => 1]), $array['payload']); + $this->assertSame(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => 1]), $array['payload']); $this->assertEquals(0, $array['attempts']); $this->assertNull($array['reserved_at']); $this->assertSame(1002, $array['available_at']); @@ -433,14 +433,14 @@ public function testBulkBatchPushesOntoDatabase(): void $query->expects('insert')->andReturnUsing(function (array $records) use ($uuid, $now): bool { $this->assertEquals([[ 'queue' => 'queue', - 'payload' => json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => null]), + 'payload' => json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => null]), 'attempts' => 0, 'reserved_at' => null, 'available_at' => 1732502704, 'created_at' => 1732502704, ], [ 'queue' => 'queue', - 'payload' => json_encode(['uuid' => $uuid, 'displayName' => 'bar', 'job' => 'bar', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => null]), + 'payload' => json_encode(['uuid' => $uuid, 'displayName' => 'bar', 'job' => 'bar', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'delay' => null]), 'attempts' => 0, 'reserved_at' => null, 'available_at' => 1732502704, diff --git a/tests/Queue/QueueJobTest.php b/tests/Queue/QueueJobTest.php index 68697e1e64..fb4c564778 100644 --- a/tests/Queue/QueueJobTest.php +++ b/tests/Queue/QueueJobTest.php @@ -17,6 +17,7 @@ use Hypervel\Queue\Events\JobFailed; use Hypervel\Queue\InvalidPayloadException; use Hypervel\Queue\Jobs\Job; +use Hypervel\Queue\SyncQueue; use Hypervel\Queue\TimeoutExceededException; use Hypervel\Tests\TestCase; use Mockery as m; @@ -176,7 +177,10 @@ public function testBatchRollbackCancellationEscapesBeforeFurtherFailureCleanup( $job = new QueueJobPayloadStub(json_encode([ 'uuid' => 'job-uuid', 'job' => QueueJobTimeoutBatchStub::class, - 'data' => ['commandName' => QueueJobTimeoutBatchStub::class], + 'data' => [ + 'commandName' => QueueJobTimeoutBatchStub::class, + 'command' => serialize(new QueueJobTimeoutBatchStub), + ], ], JSON_THROW_ON_ERROR)); $timeout = TimeoutExceededException::forJob($job); $gate = $this->armCurrentCoroutineCancellation(); @@ -258,7 +262,10 @@ public function testBatchRollbackFailurePreservesTimeoutForFailedJobCleanup(?str $job = new QueueJobPayloadStub(json_encode([ 'uuid' => 'job-uuid', 'job' => QueueJobTimeoutBatchStub::class, - 'data' => ['commandName' => QueueJobTimeoutBatchStub::class], + 'data' => [ + 'commandName' => QueueJobTimeoutBatchStub::class, + 'command' => serialize(new QueueJobTimeoutBatchStub), + ], ], JSON_THROW_ON_ERROR)); $timeout = TimeoutExceededException::forJob($job); @@ -311,6 +318,41 @@ public static function failedJobDatabases(): array ]; } + public function testStringJobTimeoutDoesNotTreatCallerDataAsBatchMetadata(): void + { + $queue = new class extends SyncQueue { + /** + * Build a string-job payload through the real producer. + */ + public function payloadFor(string $job, array $data): string + { + return $this->createPayload($job, 'default', $data); + } + }; + $job = new QueueJobPayloadStub($queue->payloadFor( + QueueJobTimeoutBatchStub::class, + ['command' => 'backup:run', 'commandName' => 'backup:run'], + )); + $timeout = TimeoutExceededException::forJob($job); + $events = m::mock(Dispatcher::class); + $events->shouldReceive('hasListeners')->once()->with(JobFailed::class)->andReturnTrue(); + $events->shouldReceive('dispatch')->once() + ->withArgs(fn (JobFailed $event): bool => $event->job === $job && $event->exception === $timeout); + + $container = m::mock(Container::class); + $container->shouldNotReceive('make')->with(BatchRepository::class); + $container->shouldReceive('make')->with('config')->andReturn(new ConfigRepository(['queue' => ['failed' => ['driver' => null]]])); + $container->shouldReceive('make')->once()->with(QueueJobTimeoutBatchStub::class)->andReturn(new QueueJobTimeoutBatchStub); + $container->shouldReceive('make')->once()->with(Dispatcher::class)->andReturn($events); + $job->setContainer($container); + $job->setConnectionName('redis'); + + $job->fail($timeout); + + $this->assertTrue($job->hasFailed()); + $this->assertTrue($job->isDeleted()); + } + /** * Arm exact cancellation of the current coroutine at a controlled channel handoff. */ diff --git a/tests/Queue/QueueListFailedCommandTest.php b/tests/Queue/QueueListFailedCommandTest.php index 0c8ffbff80..bc63556acf 100644 --- a/tests/Queue/QueueListFailedCommandTest.php +++ b/tests/Queue/QueueListFailedCommandTest.php @@ -7,6 +7,7 @@ use Hypervel\Foundation\Application; use Hypervel\Queue\Console\ListFailedCommand; use Hypervel\Queue\Failed\FailedJobProviderInterface; +use Hypervel\Queue\SyncQueue; use Hypervel\Tests\TestCase; use Mockery as m; use Symfony\Component\Console\Input\ArrayInput; @@ -84,6 +85,24 @@ public function testEncryptedJobShowsUnderlyingClass(): void $this->assertStringNotContainsString('CallQueuedHandler', $output); } + public function testStringJobCommandDataDoesNotHideTheHandlerMethod(): void + { + $queue = new class extends SyncQueue { + /** + * Build a string payload through the real queue producer. + */ + public function payloadFor(string $job, array $data): string + { + return $this->createPayload($job, 'default', $data); + } + }; + $output = $this->runListFailedCommandWithRawPayload( + $queue->payloadFor('RunCommand@handle', ['command' => 'backup:run']), + ); + + $this->assertStringContainsString('RunCommand@handle', $output); + } + public function testMalformedPayloadDoesNotThrow(): void { // Malformed JSON in the payload column must not bubble up an exception; diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index fdeb94e40d..53dff20845 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -381,7 +381,7 @@ public function testPushProperlyPushesJobOntoRedis(): void $redisProxy = m::mock(RedisProxy::class); $redisProxy->shouldAllowMockingMethod('evalWithShaCache'); $redisProxy->expects('isCluster')->andReturn(false); - $redisProxy->expects('evalWithShaCache')->with(LuaScripts::push(), ['queues:default', 'queues:default:notify'], [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null])]); + $redisProxy->expects('evalWithShaCache')->with(LuaScripts::push(), ['queues:default', 'queues:default:notify'], [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null])]); $redis->expects('connection')->twice()->andReturn($redisProxy); $id = $queue->push('foo', ['data']); @@ -404,7 +404,7 @@ public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook(): void $redisProxy = m::mock(RedisProxy::class); $redisProxy->shouldAllowMockingMethod('evalWithShaCache'); $redisProxy->expects('isCluster')->andReturn(false); - $redisProxy->expects('evalWithShaCache')->with(LuaScripts::push(), ['queues:default', 'queues:default:notify'], [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'custom' => 'taylor', 'id' => 'foo', 'attempts' => 0, 'delay' => null])]); + $redisProxy->expects('evalWithShaCache')->with(LuaScripts::push(), ['queues:default', 'queues:default:notify'], [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'custom' => 'taylor', 'id' => 'foo', 'attempts' => 0, 'delay' => null])]); $redis->expects('connection')->twice()->andReturn($redisProxy); Queue::createPayloadUsing(function (string $connection, ?string $queue, array $payload): array { @@ -432,7 +432,7 @@ public function testJobQueueingAndQueuedEventsAreSkippedWhenNoListenersAreRegist $redisProxy = m::mock(RedisProxy::class); $redisProxy->shouldAllowMockingMethod('evalWithShaCache'); $redisProxy->expects('isCluster')->andReturn(false); - $redisProxy->expects('evalWithShaCache')->with(LuaScripts::push(), ['queues:default', 'queues:default:notify'], [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null])]); + $redisProxy->expects('evalWithShaCache')->with(LuaScripts::push(), ['queues:default', 'queues:default:notify'], [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null])]); $redis->expects('connection')->twice()->andReturn($redisProxy); $events = m::mock(Dispatcher::class); @@ -562,7 +562,7 @@ public function testPushRaisesFailedEventWhenRedisThrows(): void $this->assertSame('foo', $event->job); $this->assertSame('default', $event->connectionName); $this->assertNull($event->queue); - $this->assertSame(['uuid' => (string) $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null], $event->payload()); + $this->assertSame(['uuid' => (string) $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null], $event->payload()); return true; }) @@ -602,7 +602,7 @@ public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook(): vo $redisProxy = m::mock(RedisProxy::class); $redisProxy->shouldAllowMockingMethod('evalWithShaCache'); $redisProxy->expects('isCluster')->andReturn(false); - $redisProxy->expects('evalWithShaCache')->with(LuaScripts::push(), ['queues:default', 'queues:default:notify'], [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'custom' => 'taylor', 'bar' => 'foo', 'id' => 'foo', 'attempts' => 0, 'delay' => null])]); + $redisProxy->expects('evalWithShaCache')->with(LuaScripts::push(), ['queues:default', 'queues:default:notify'], [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'custom' => 'taylor', 'bar' => 'foo', 'id' => 'foo', 'attempts' => 0, 'delay' => null])]); $redis->expects('connection')->twice()->andReturn($redisProxy); Queue::createPayloadUsing(function (string $connection, ?string $queue, array $payload): array { @@ -639,7 +639,7 @@ public function testDelayedPushProperlyPushesJobOntoRedis(): void $redisProxy->expects('evalWithShaCache')->with( LuaScripts::later(), ['queues:default:delayed'], - [1002, json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1])] + [1002, json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1])] ); $redis->expects('connection')->twice()->andReturn($redisProxy); @@ -668,7 +668,7 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis(): void $redisProxy->expects('evalWithShaCache')->with( LuaScripts::later(), ['queues:default:delayed'], - [1002, json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1])] + [1002, json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1])] ); $redis->expects('connection')->twice()->andReturn($redisProxy); @@ -694,7 +694,7 @@ public function testDelayedPushWithIntervalNeverRunsBeforeRequestedLifetime(): v $redisProxy->expects('evalWithShaCache')->with( LuaScripts::later(), ['queues:default:delayed'], - [1002, json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1])] + [1002, json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1])] ); $redis->expects('connection')->twice()->andReturn($redisProxy); @@ -853,7 +853,7 @@ public function testPushUsesGetRedisKeyForLuaScript(): void $redisProxy->expects('evalWithShaCache')->with( LuaScripts::push(), ['queues:{default}', 'queues:{default}:notify'], - [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null])] + [json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null])] ); $redis->expects('connection')->twice()->andReturn($redisProxy); @@ -920,7 +920,7 @@ public function testLaterUsesClusterSafeRedisKeyForDelayedSet(): void $redisProxy->expects('evalWithShaCache')->with( LuaScripts::later(), ['queues:{default}:delayed'], - [2, json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1])] + [2, json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'stringJob' => true, 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $now->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1])] ); $redis->expects('connection')->twice()->andReturn($redisProxy); diff --git a/tests/Queue/QueueRetryCommandTest.php b/tests/Queue/QueueRetryCommandTest.php index 8a0dfca668..7e01a5ca5e 100644 --- a/tests/Queue/QueueRetryCommandTest.php +++ b/tests/Queue/QueueRetryCommandTest.php @@ -4,9 +4,12 @@ namespace Hypervel\Tests\Queue; +use Aws\Result; +use Aws\Sqs\SqsClient; use Generator; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Queue\Queue as QueueContract; +use Hypervel\Database\Eloquent\ModelNotFoundException; use Hypervel\Foundation\Application; use Hypervel\Queue\Console\RetryCommand; use Hypervel\Queue\Events\JobRetryRequested; @@ -14,10 +17,12 @@ use Hypervel\Queue\Failed\NullFailedJobProvider; use Hypervel\Queue\QueueManager; use Hypervel\Queue\SqsQueue; +use Hypervel\Queue\SyncQueue; use Hypervel\Support\Collection; use Hypervel\Support\LazyCollection; use Hypervel\Tests\TestCase; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\TestWith; use RuntimeException; use stdClass; @@ -160,7 +165,9 @@ public function testItResetsAttemptsCountWhenRetryingAJob(): void $this->runRetryCommand(['id' => ['1']], $failer, ['database' => $queue]); } - public function testRefreshesTheRetryUntilTimestampWhenTheJobDefinesRetryUntil(): void + #[TestWith([false], 'unmarked object payload')] + #[TestWith([true], 'custom object handler')] + public function testRefreshesTheRetryUntilTimestampWhenTheJobDefinesRetryUntil(bool $customHandler): void { $failer = m::mock(FailedJobProviderInterface::class); $queue = m::mock(QueueContract::class); @@ -173,6 +180,14 @@ public function testRefreshesTheRetryUntilTimestampWhenTheJobDefinesRetryUntil() job: new QueueRetryCommandTestJobWithRetryUntil(retryUntil: 1234567890) ); + if ($customHandler) { + $producer = new QueueRetryCommandCustomQueue; + $producer->setContainer(new Application); + $payload = json_decode($producer->payloadFor(new QueueRetryCommandTestJobWithRetryUntil(1234567890)), true); + $payload['retryUntil'] = 0; + $job->payload = json_encode($payload); + } + $failer->shouldReceive('find')->once()->with('1')->andReturn($job); $queue->shouldReceive('pushRaw')->once()->with(m::on(function (string $payload): bool { return json_decode($payload, true)['retryUntil'] === 1234567890; @@ -533,6 +548,115 @@ public function testStopsRetryingWhenAJobInACollectionFailsToBePushed(): void $this->assertSame(['job-3'], array_keys($pendingJobs)); } + public function testRetriesRemainingJobsWhenAJobsModelNoLongerExists(): void + { + $failer = m::mock(FailedJobProviderInterface::class); + $queue = m::mock(QueueContract::class); + + $missing = $this->failedJob(id: 'job-1', connection: 'database', queue: 'default', job: new QueueRetryCommandTestJobWithMissingModel); + + $jobs = new Collection([ + 'job-1' => $missing, + 'job-2' => $this->failedJob(id: 'job-2', connection: 'database', queue: 'default'), + ]); + + $failer->shouldReceive('find')->once()->with('batch')->andReturn($jobs); + + $queue->shouldReceive('pushRaw')->once()->with($missing->payload, 'default', []); + $failer->shouldReceive('forget')->once()->with('job-1'); + + $queue->shouldReceive('pushRaw')->once()->with($this->retriedPayload(id: 'job-2'), 'default', []); + $failer->shouldReceive('forget')->once()->with('job-2'); + + $this->runRetryCommand(['id' => ['batch']], $failer, ['database' => $queue]); + } + + public function testRetriesStringJobsWithoutInterpretingTheirCommandData(): void + { + $failer = m::mock(FailedJobProviderInterface::class); + $queue = m::mock(QueueContract::class); + $data = ['command' => 'backup:run', 'commandName' => 'backup:run']; + $job = $this->failedJob(id: '1', connection: 'database', queue: 'default'); + $job->payload = (new QueueRetryCommandCustomQueue)->payloadFor('RunCommand@handle', $data); + + $failer->shouldReceive('find')->once()->with('1')->andReturn($job); + $queue->shouldReceive('pushRaw')->once()->with($job->payload, 'default', []); + $failer->shouldReceive('forget')->once()->with('1'); + + $this->runRetryCommand(['id' => ['1']], $failer, ['database' => $queue]); + } + + public function testRetriesJobWithoutQueueableOptionsWhenItsModelNoLongerExists(): void + { + $failer = m::mock(FailedJobProviderInterface::class); + $sqs = m::mock(SqsClient::class); + $queue = new SqsQueue($sqs, 'default', 'https://sqs.example.test'); + + $job = $this->failedJob(id: '1', connection: 'sqs', queue: 'default', job: new QueueRetryCommandTestJobWithMissingModel); + + $failer->shouldReceive('find')->once()->with('1')->andReturn($job); + $sqs->shouldReceive('sendMessage')->once()->with([ + 'QueueUrl' => 'https://sqs.example.test/default', + 'MessageBody' => $job->payload, + ])->andReturn(new Result(['MessageId' => 'message-1'])); + $failer->shouldReceive('forget')->once()->with('1'); + + $this->runRetryCommand(['id' => ['1']], $failer, ['sqs' => $queue]); + } + + #[DataProvider('jobsWithoutRestorableObjects')] + public function testRetriesSqsJobsWithDefaultOptionsWhenNoObjectCanBeRestored(string $queueName, array $payload): void + { + $failer = m::mock(FailedJobProviderInterface::class); + $sqs = m::mock(SqsClient::class); + $queue = new SqsQueue($sqs, $queueName, 'https://sqs.example.test'); + $job = $this->failedJob( + id: '1', + connection: 'sqs', + queue: $queueName, + payload: $payload, + job: new QueueRetryCommandTestJobWithMissingModel, + ); + + $failer->shouldReceive('find')->once()->with('1')->andReturn($job); + $sqs->shouldReceive('sendMessage')->once()->with(m::on(function (array $message) use ($queueName, $job): bool { + $expected = [ + 'QueueUrl' => 'https://sqs.example.test/' . $queueName, + 'MessageBody' => $job->payload, + ]; + + if (str_ends_with($queueName, '.fifo')) { + $this->assertIsString($message['MessageDeduplicationId']); + unset($message['MessageDeduplicationId']); + $expected['MessageGroupId'] = $queueName; + } + + $this->assertSame($expected, $message); + + return true; + }))->andReturn(new Result(['MessageId' => 'message-1'])); + $failer->shouldReceive('forget')->once()->with('1'); + + $this->runRetryCommand(['id' => ['1']], $failer, ['sqs' => $queue]); + } + + /** + * Provide retry payloads whose queue options need no restored object. + */ + public static function jobsWithoutRestorableObjects(): iterable + { + yield 'missing model on FIFO' => ['default.fifo', []]; + yield 'string job with command data on FIFO' => ['default.fifo', json_decode( + (new QueueRetryCommandCustomQueue)->payloadFor('RunCommand@handle', ['command' => 'backup:run', 'commandName' => 'backup:run']), + true, + )]; + + $incomplete = ['data' => ['commandName' => 'RemovedJob', 'command' => 'O:10:"RemovedJob":0:{}']]; + + yield 'removed class on FIFO' => ['default.fifo', $incomplete]; + yield 'removed class on standard' => ['default', $incomplete]; + } + public function testDispatchesTheEventBeforePushingAndForgetsTheJobAfterwards(): void { $failer = m::mock(FailedJobProviderInterface::class); @@ -711,6 +835,25 @@ private function runRetryCommand(array $input, FailedJobProviderInterface $faile } } +class QueueRetryCommandCustomQueue extends SyncQueue +{ + /** + * Build payloads through the real queue producer. + */ + public function payloadFor(object|string $job, mixed $data = ''): string + { + return $this->createPayload($job, 'default', $data); + } + + /** + * Replace the object handler as a custom queue driver may do. + */ + protected function createObjectPayload(object $job, ?string $queue): array + { + return array_replace(parent::createObjectPayload($job, $queue), ['job' => 'CustomObjectHandler@call']); + } +} + class QueueRetryCommandTestJob { } @@ -732,3 +875,14 @@ public function retryUntil(): int return $this->retryUntil; } } + +class QueueRetryCommandTestJobWithMissingModel +{ + /** + * Restore a job whose model has been deleted. + */ + public function __unserialize(array $data): void + { + throw (new ModelNotFoundException)->setModel('App\Models\User', [1]); + } +} diff --git a/tests/Telescope/Watchers/JobWatcherTest.php b/tests/Telescope/Watchers/JobWatcherTest.php index d6295f5e40..fd367a7b07 100644 --- a/tests/Telescope/Watchers/JobWatcherTest.php +++ b/tests/Telescope/Watchers/JobWatcherTest.php @@ -28,7 +28,9 @@ use Hypervel\Testbench\Attributes\WithMigration; use Hypervel\Testbench\Factories\UserFactory; use Hypervel\Tests\Telescope\FeatureTestCase; +use JsonSerializable; use Mockery as m; +use PHPUnit\Framework\Attributes\TestWith; use RuntimeException; use Throwable; @@ -175,6 +177,32 @@ public function testItHandlesPushedJobs(): void $this->assertSame(['framework' => 'Laravel'], $entry->content['data']); } + #[TestWith([ + ['command' => 'backup:run', 'commandName' => 'backup:run', 'batchId' => ['external' => 42]], + ['command' => 'backup:run', 'commandName' => 'backup:run', 'batchId' => ['external' => 42]], + ], 'caller metadata names')] + #[TestWith(['', ['']], 'default string data')] + #[TestWith([new PushedJobData, ['command' => 'backup:run']], 'JSON serializable data')] + public function testStringJobsPreserveCallerDataWithoutInterpretingItAsMetadata(mixed $data, array $expectedData): void + { + $this->app->make(QueueManager::class) + ->connection('database') + ->push(MyPushedJobClass::class . '@handle', $data); + $this->artisan('queue:work', [ + 'connection' => 'database', + '--once' => true, + ]); + + $entry = $this->loadTelescopeEntries()->first(); + $this->assertSame(EntryType::JOB, $entry->type); + $this->assertSame('processed', $entry->content['status']); + $this->assertSame('database', $entry->content['connection']); + $this->assertSame(MyPushedJobClass::class, $entry->content['name']); + $this->assertSame('default', $entry->content['queue']); + $this->assertSame($expectedData, $entry->content['data']); + $this->assertNull($entry->family_hash); + } + public function testJobCanHandleDeletedSerializedModel(): void { $user = UserFactory::new()->create(); @@ -431,6 +459,24 @@ public function fire(Job $job, array $data) { throw new PushedJobFailedException; } + + /** + * Handle a string job successfully. + */ + public function handle(Job $job, mixed $data): void + { + } +} + +class PushedJobData implements JsonSerializable +{ + /** + * Publish caller data independently of the object's properties. + */ + public function jsonSerialize(): array + { + return ['command' => 'backup:run']; + } } class PushedJobFailedException extends Exception From 31f71cd6f9a9f7ca6d00b43880c7d1bdd25da21a Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:29:45 +0000 Subject: [PATCH 04/36] Clear the current route before each coroutine-local dispatch Port laravel/framework#62123 from 835946cb1608a309be92141feae6be438639fbff, reconciled against the frozen 13.x source. A route miss must not expose the route from an earlier dispatch in the same coroutine. Reset only the coroutine-local current route at the public dispatch boundary, preserving request isolation and dispatchToRoute customization. Port every upstream regression assertion; the test fails before the reset and the complete RoutingRouteTest passes afterward. Static analysis and formatting pass. --- src/routing/src/Router.php | 2 ++ tests/Routing/RoutingRouteTest.php | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/routing/src/Router.php b/src/routing/src/Router.php index 48e0362529..f9b9e93ace 100644 --- a/src/routing/src/Router.php +++ b/src/routing/src/Router.php @@ -609,6 +609,8 @@ public function respondWithRoute(string $name): SymfonyResponse public function dispatch(Request $request): SymfonyResponse { CoroutineContext::set(self::CURRENT_REQUEST_CONTEXT_KEY, $request); + // A failed match must not retain an earlier dispatch's route in this coroutine. + CoroutineContext::set(self::CURRENT_ROUTE_CONTEXT_KEY, null); return $this->dispatchToRoute($request); } diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index a11ad345f0..9c16d29fce 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -820,6 +820,26 @@ public function testRoutesDontMatchNonMatchingPathsWithLeadingOptionals(): void $this->assertSame('25', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); } + public function testCurrentRouteIsResetWhenTheNextRequestDoesNotMatch(): void + { + $router = $this->getRouter(); + $router->get('invoices/{invoice}', fn (string $invoice): string => $invoice)->name('invoices.show'); + + $this->assertSame('123', $router->dispatch(Request::create('invoices/123', 'GET'))->getContent()); + $this->assertSame('invoices.show', $router->currentRouteName()); + + try { + $router->dispatch(Request::create('missing', 'GET')); + + $this->fail('A NotFoundHttpException should have been thrown.'); + } catch (NotFoundHttpException) { + } + + $this->assertNull($router->current()); + $this->assertNull($router->currentRouteName()); + $this->assertFalse($router->is('invoices.show')); + } + public function testRoutesDontMatchNonMatchingDomain(): void { $this->expectException(NotFoundHttpException::class); From 2a0c7c0eaa9df50a3a98f7f90d10b121c801a2f0 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:24:35 +0000 Subject: [PATCH 05/36] Cover Redis tag pruning with SCAN_PREFIX Port laravel/framework#62122 from dd4184fd9306 at the recorded 13.x target 5d7797528723107cd4f495326838a7d7d67fbb20. Exercise the existing SafeScan path with a prefixed phpredis connection, preserving logical match patterns and prefix-stripped tag removal. Keep the native pruning implementation. Validation: complete CacheRedisStoreTest passes (20 tests, 62 assertions); batch formatting and source/type analysis pass. --- tests/Cache/CacheRedisStoreTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Cache/CacheRedisStoreTest.php b/tests/Cache/CacheRedisStoreTest.php index a575f40a27..d9cb87f375 100755 --- a/tests/Cache/CacheRedisStoreTest.php +++ b/tests/Cache/CacheRedisStoreTest.php @@ -12,6 +12,7 @@ use Hypervel\Tests\Cache\Redis\RedisCacheTestCase; use Hypervel\Tests\Redis\Fixtures\FakeRedisClient; use Mockery as m; +use Redis; use stdClass; class CacheRedisStoreTest extends RedisCacheTestCase @@ -256,4 +257,24 @@ public function testFlushStaleTagsStopsScanningWhenTheCursorReachesZero(): void $this->assertSame(1, $client->getScanCallCount()); } + + public function testFlushStaleTagsDoesNotDoublePrefixTheMatchPattern(): void + { + $connection = $this->mockConnection(); + $connection->allows('getShouldTransform')->andReturn(false); + $connection->allows('getOption')->with(Redis::OPT_PREFIX)->andReturn('app_'); + $connection->allows('getOption')->with(Redis::OPT_SCAN)->andReturn(Redis::SCAN_PREFIX); + $connection->expects('scan') + ->with(m::any(), 'prefix:_all:tag:*:entries', 1000) + ->andReturnUsing(function (mixed &$cursor): array { + $cursor = 0; + + return ['app_prefix:_all:tag:foo:entries']; + }); + $connection->expects('zRemRangeByScore')->with('prefix:_all:tag:foo:entries', '0', m::any())->andReturn(0); + $connection->allows('zscan')->andReturn(false); + $connection->allows('zCard')->andReturn(0); + + $this->createStore($connection, tagMode: 'all')->flushStaleTags(); + } } From 10156b97109e8d80a082c6148653b55e021ff516 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:24:35 +0000 Subject: [PATCH 06/36] Add the session garbage collection command Port laravel/framework#62104 and #62150 from 61073964b7c5 and d8f93dacc8a0 at 13.x target 5d7797528723107cd4f495326838a7d7d67fbb20. Register session:garbage-collect through the package provider, pass the configured session lifetime to the handler, and document scheduled cleanup and Redis expiry behavior. Validation: registered-command test passes (1 test, 3 assertions), command signatures pass (115 tests, 690 assertions), and batch source/type/skeleton analysis passes. --- src/docs/session.md | 20 +++++++++++ .../Console/SessionGarbageCollectCommand.php | 35 +++++++++++++++++++ src/session/src/SessionServiceProvider.php | 1 + tests/Console/Fixtures/command_signatures.php | 3 ++ .../SessionGarbageCollectCommandTest.php | 27 ++++++++++++++ 5 files changed, 86 insertions(+) create mode 100644 src/session/src/Console/SessionGarbageCollectCommand.php create mode 100644 tests/Session/SessionGarbageCollectCommandTest.php diff --git a/src/docs/session.md b/src/docs/session.md index c50325975e..9106ce903a 100644 --- a/src/docs/session.md +++ b/src/docs/session.md @@ -3,6 +3,7 @@ - [Introduction](#introduction) - [Configuration](#configuration) - [Driver Prerequisites](#driver-prerequisites) + - [Garbage Collection](#garbage-collection) - [Interacting With the Session](#interacting-with-the-session) - [Retrieving Data](#retrieving-data) - [Storing Data](#storing-data) @@ -88,6 +89,25 @@ Redis stores session payloads directly rather than routing them through a cache To list and invalidate a user's Redis sessions, enable `SESSION_TRACK_USER_SESSIONS`. This feature requires phpredis 6.3.0 or later and Redis 8.0 or Valkey 9.0 or later. You may leave this option disabled if your application does not provide session-management controls. + +### Garbage Collection + +The file and database session drivers remove expired sessions using a request-based lottery. You may run cleanup explicitly using the `session:garbage-collect` Artisan command: + +```shell +php artisan session:garbage-collect +``` + +To move cleanup out of requests, [schedule the command](/docs/{{version}}/scheduling) and set the `lottery` option in `config/session.php` to `[0, 100]`: + +```php +use Hypervel\Support\Facades\Schedule; + +Schedule::command('session:garbage-collect')->hourly(); +``` + +The command uses your configured session driver and lifetime. Redis sessions expire automatically and do not need scheduled garbage collection. + ## Interacting With the Session diff --git a/src/session/src/Console/SessionGarbageCollectCommand.php b/src/session/src/Console/SessionGarbageCollectCommand.php new file mode 100644 index 0000000000..332543b89c --- /dev/null +++ b/src/session/src/Console/SessionGarbageCollectCommand.php @@ -0,0 +1,35 @@ +hypervel->make('session') + ->driver() + ->getHandler() + ->gc($this->hypervel->make('config')->integer('session.lifetime') * 60); + + $this->components->info('Session garbage collector ran successfully.'); + } +} diff --git a/src/session/src/SessionServiceProvider.php b/src/session/src/SessionServiceProvider.php index 119d86c980..7bd22f2f4c 100644 --- a/src/session/src/SessionServiceProvider.php +++ b/src/session/src/SessionServiceProvider.php @@ -17,6 +17,7 @@ public function register(): void $this->registerSessionDriver(); $this->commands([ + Console\SessionGarbageCollectCommand::class, Console\SessionTableCommand::class, ]); } diff --git a/tests/Console/Fixtures/command_signatures.php b/tests/Console/Fixtures/command_signatures.php index 83db31378d..54c8818a5a 100644 --- a/tests/Console/Fixtures/command_signatures.php +++ b/tests/Console/Fixtures/command_signatures.php @@ -4225,6 +4225,9 @@ ], ], ], + \Hypervel\Session\Console\SessionGarbageCollectCommand::class => [ + 'name' => 'session:garbage-collect', + ], \Hypervel\Session\Console\SessionTableCommand::class => [ 'name' => 'make:session-table', 'aliases' => [ diff --git a/tests/Session/SessionGarbageCollectCommandTest.php b/tests/Session/SessionGarbageCollectCommandTest.php new file mode 100644 index 0000000000..531ef571b0 --- /dev/null +++ b/tests/Session/SessionGarbageCollectCommandTest.php @@ -0,0 +1,27 @@ +expects('gc')->with(420)->andReturn(2); + $this->app->make(SessionManager::class)->extend('custom', fn () => $handler); + + $this->artisan('session:garbage-collect') + ->expectsOutputToContain('Session garbage collector ran successfully.') + ->assertSuccessful(); + } +} From 9f8892c8b22bd71c6cf19681d78c6a8812576a1e Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:24:35 +0000 Subject: [PATCH 07/36] Update the shared Vite toolchain and rebuild frontend assets Port laravel/framework#62132 (1e8b7383762e) and laravel/ai#1166 (ac4b35b00c714a91c4f76d246898d0d77f84deac). Use Vite+ 1.4.0, which bundles Vite 8.3.5 and Rolldown 1.2.15, through the existing workspace catalog and override. Rebuild the renderer, Horizon, Telescope and Workbench assets without adding another Vite installation. Validation: frozen dependency installation, frontend source checks, all five builds, 256 JavaScript tests, 159 cached-route tests and generated Wayfinder type checking pass. --- pnpm-lock.yaml | 895 +++++++++++------- pnpm-workspace.yaml | 6 +- .../exceptions/renderer/dist/scripts.js | 6 +- src/horizon/dist/app.js | 16 +- src/telescope/dist/app.js | 16 +- .../public/build/assets/app-CvQ5XWTR.js | 5 - .../public/build/assets/app-RtO928pj.js | 5 + src/workbench/public/build/manifest.json | 2 +- 8 files changed, 556 insertions(+), 395 deletions(-) delete mode 100644 src/workbench/public/build/assets/app-CvQ5XWTR.js create mode 100644 src/workbench/public/build/assets/app-RtO928pj.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fc31bd114..790220bbba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,11 +55,11 @@ catalogs: specifier: ^7.0.2 version: 7.0.2 vite: - specifier: npm:@voidzero-dev/vite-plus-core@1.3.0 - version: 1.3.0 + specifier: npm:@voidzero-dev/vite-plus-core@1.4.0 + version: 1.4.0 vite-plus: - specifier: 1.3.0 - version: 1.3.0 + specifier: 1.4.0 + version: 1.4.0 vue: specifier: ^3.5.44 version: 3.5.44 @@ -71,7 +71,7 @@ catalogs: version: 4.6.4 overrides: - vite@*: npm:@voidzero-dev/vite-plus-core@1.3.0 + vite@*: npm:@voidzero-dev/vite-plus-core@1.4.0 vitest@*: 5.0.5 importers: @@ -89,7 +89,7 @@ importers: version: 7.0.2 vite-plus: specifier: 'catalog:' - version: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + version: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) dogfood/ai/workbench: dependencies: @@ -120,22 +120,22 @@ importers: devDependencies: '@tailwindcss/vite': specifier: 'catalog:' - version: 4.3.3(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + version: 4.3.3(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) '@vitejs/plugin-react': specifier: ^6.0.3 - version: 6.1.2(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + version: 6.1.2(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) laravel-vite-plugin: specifier: 'catalog:' - version: 3.2.0(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + version: 3.2.0(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) tailwindcss: specifier: 'catalog:' version: 4.3.3 vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + version: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' vite-plus: specifier: 'catalog:' - version: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + version: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) src/foundation/resources/exceptions/renderer: dependencies: @@ -163,13 +163,13 @@ importers: devDependencies: '@tailwindcss/vite': specifier: 'catalog:' - version: 4.3.3(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + version: 4.3.3(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + version: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' vite-plus: specifier: 'catalog:' - version: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + version: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) src/horizon: devDependencies: @@ -178,7 +178,7 @@ importers: version: 2.11.8 '@vitejs/plugin-vue': specifier: 'catalog:' - version: 6.0.10(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vue@3.5.44(typescript@7.0.2)) + version: 6.0.10(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vue@3.5.44(typescript@7.0.2)) '@vue/compiler-dom': specifier: 'catalog:' version: 3.5.44 @@ -214,10 +214,10 @@ importers: version: 1.106.0 vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + version: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' vite-plus: specifier: 'catalog:' - version: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + version: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) vue: specifier: 'catalog:' version: 3.5.44(typescript@7.0.2) @@ -235,7 +235,7 @@ importers: version: 2.11.8 '@vitejs/plugin-vue': specifier: 'catalog:' - version: 6.0.10(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vue@3.5.44(typescript@7.0.2)) + version: 6.0.10(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vue@3.5.44(typescript@7.0.2)) '@vue/compiler-dom': specifier: 'catalog:' version: 3.5.44 @@ -274,10 +274,10 @@ importers: version: 15.9.0 vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + version: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' vite-plus: specifier: 'catalog:' - version: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + version: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) vue: specifier: 'catalog:' version: 3.5.44(typescript@7.0.2) @@ -301,7 +301,7 @@ importers: version: 7.0.2 vite-plus: specifier: 'catalog:' - version: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + version: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) src/workbench: devDependencies: @@ -316,7 +316,7 @@ importers: version: 10.6.1(postcss@8.5.31) laravel-vite-plugin: specifier: 'catalog:' - version: 3.2.0(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + version: 3.2.0(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) postcss: specifier: ^8.5.28 version: 8.5.31 @@ -325,10 +325,10 @@ importers: version: 3.4.19 vite: specifier: 'catalog:' - version: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + version: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' vite-plus: specifier: 'catalog:' - version: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + version: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) packages: @@ -470,131 +470,131 @@ packages: '@one-ini/wasm@0.2.1': resolution: {integrity: sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==} - '@oxc-project/runtime@0.154.0': - resolution: {integrity: sha512-cBQNGoh659BNgvA8OC/e4BNDtFkv03dJaYdkp2adgOQNtISNAPQ+nB4S8XvWtNk7Xl+0g+7aa+iRGR25Xycd1Q==} + '@oxc-project/runtime@0.155.0': + resolution: {integrity: sha512-VbW9raczGS5ZZ1FMunUBYHe4uV/R684qlQCoMdE3n5JT1PcqQsvmFpMYkF8AD+W04fBLGAq6Tr7YsTk0jjvDBA==} engines: {node: ^20.19.0 || >=22.12.0} - '@oxc-project/types@0.154.0': - resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==} + '@oxc-project/types@0.155.0': + resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} - '@oxfmt/binding-android-arm-eabi@0.73.0': - resolution: {integrity: sha512-dhunAdMbi/4o0Y66/+Wl6rpsNqpvE4i26Qw6bB77A97L2lJoktWg6m+X9QOon8iOEEqzD9rEZqFi5uZ4oIzhCA==} + '@oxfmt/binding-android-arm-eabi@0.74.0': + resolution: {integrity: sha512-xxQwrf5EPI5zxvuCTW8r6tzCTjPQ+h6+HqJ0iRKRy2zS1yBllt/xLVjoIyUc+FnR7g0a5g1SZv2yyDYhTItLbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.73.0': - resolution: {integrity: sha512-dBoypjC2/xeyKyIhN2waX0/XeoccH9k0lVVe+sGRc4X8HSJVRtqyZDoXdr7bGY9A75c51dX0z0dLDpGQKwCdCg==} + '@oxfmt/binding-android-arm64@0.74.0': + resolution: {integrity: sha512-FJW3sATVvrWbV64al8xofCPZj4500Z/1W0ad7Tg1rcCUyh0EMbuSrsf3jsZS7CEbwQ5fQ0mRhmkc129GFgofSQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.73.0': - resolution: {integrity: sha512-Tn7Y+qWIpCQK+qdYwBcn6YvqeBuPiuG3tcNlYg16a5QL2o3rSmrgBB1nCyYqo0uvc1LNrfQW9/CK2tzFuID/Fg==} + '@oxfmt/binding-darwin-arm64@0.74.0': + resolution: {integrity: sha512-TfmNKBD30dGyMu9GCLiaw2rt/z7fms9LS+5cKcNOyWst0QgAs8oZs6wPxMbVaaDVE0/PCdCosgWM1vbu6f5NUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.73.0': - resolution: {integrity: sha512-yVb0E0tg/+9Xc9YJCBq66qF1DC/aN5mhb5nnHz0k4ikwkMANtSul2VPDSwjYdXR6/DFimc+pPUgLBqkIa5Ctuw==} + '@oxfmt/binding-darwin-x64@0.74.0': + resolution: {integrity: sha512-5Usf8+DfC4Q3o/MD8FkN47GrI6y0KbhQ4KhZ2HGzMZcXjcW1AeldwbUn51aKih+1TVuJFF9qkNm6X1nzm6NYng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.73.0': - resolution: {integrity: sha512-IKR4lFnBkYNL9Uqdxa5GMTQLvnLj+3sDwTvh2atHCzah0xeo+1xR5bigRU8uyXlOsA4VpXUn0UAimq4E4RBGmQ==} + '@oxfmt/binding-freebsd-x64@0.74.0': + resolution: {integrity: sha512-eL+1O3gykw1XihbZh2MRAciQvU2988LByrnjhyo/dmNG74X4oZ7XzigRoWPRFLfJCp8hzRkp/LbEhwe7uuT2eA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.73.0': - resolution: {integrity: sha512-7qnZ4g0ZCO3aZevOOQpZIABYwwXJW1lLDo2elviGdPQ0i11255xKEMFsi97fC1/nauQFseCJfELUy/k2JoOP+g==} + '@oxfmt/binding-linux-arm-gnueabihf@0.74.0': + resolution: {integrity: sha512-Xt0QtO2hJ4stnul8IL4LJGVLyp0xnc6ccN9fZ30yuubv8FWSRYxWwd1Za62KsEqAfmPSazoS/cLSUUJpja6npA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.73.0': - resolution: {integrity: sha512-36eBico2j/WPB84gb4FYd/jjqGCKOwU4M+TWLBUIqlnzTozQueLy0joA5BwsSp2OZFVkOlpCwvA3ndPamdiyLw==} + '@oxfmt/binding-linux-arm-musleabihf@0.74.0': + resolution: {integrity: sha512-/4wYQ0gZl4vUR0QoBTGaay73FmQ1GAjUa9z9101TR2xbGNrg4a+grazlR0yzEOe3/E+PJr5BGfj66MMzm/54vg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.73.0': - resolution: {integrity: sha512-VZ3cSgCAT18knKo52DcACmAuIoqG6qVEyeZRFbJd0xmYQ8GyYjuBc6IAh6MZHijT6q7p4/HPI+6ZWsCuhdV6iw==} + '@oxfmt/binding-linux-arm64-gnu@0.74.0': + resolution: {integrity: sha512-846bKzF+HJd+JkOFh3rxzmYvSq4Cm+GMT+p+mW7S13ZYo6GAAGv8bL8cAOi/XcdeMqxEJDCIMTVQLVBXHMCdig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.73.0': - resolution: {integrity: sha512-8YVzH59XSHcqwXNgc5qK851zCqwpmBgOifARXEPPe5/xTsIbHceYoPZAyqfpYV0CfKmzSaEDBgYHFza4rhx+Zw==} + '@oxfmt/binding-linux-arm64-musl@0.74.0': + resolution: {integrity: sha512-nI5FJwZ4Flo6CBdscpwxymZXxO3m3IErQJX55ZS6h+ObfGXu8FostL85L/RWPFaCLnIVE4/WfLw+2q8XvVEeUA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.73.0': - resolution: {integrity: sha512-MqRJnmCys0rwOgpWyEidSA7v/bvOqfNDJhPkQoR9mPVYrvSmMaW2Xhdpl28cbTA+brOM1is62GjIvJplVlUVpw==} + '@oxfmt/binding-linux-ppc64-gnu@0.74.0': + resolution: {integrity: sha512-BoQYKuqUdsKAFAkK6DLBvkdv4+t0hppSm79sB0wIcE2o/eOwNj+6Z3OlKY4q3Eta/tlssP65FaS9vHn8TeF7vQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.73.0': - resolution: {integrity: sha512-4t2F3SWJSK/O/kRRFnL0elJzn75fJGGu71gs1HtiWcS80SmJOpkHiwwLnTp9suGdns8VHEPxYS0GTcyVwHKWBA==} + '@oxfmt/binding-linux-riscv64-gnu@0.74.0': + resolution: {integrity: sha512-FSroRrM12+jCdSQklF9aOxsKpI6wFs4ZCo/1cSYG80RWZS6z4LV+l9tXRMT8lDGu+yohIZBwi9M76FzGPFDE2A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.73.0': - resolution: {integrity: sha512-D8b4I8I1kLdJQFwdFXNV+cwh3xWSiNKofjdGUk0nDpc0AHOds8T+poGbP9RWQMvgsvMb9OMgp/aV7ahBR37TbQ==} + '@oxfmt/binding-linux-riscv64-musl@0.74.0': + resolution: {integrity: sha512-0VLQD0dhGBLY9Ik/w+otMvD81D0YXi4VDwql65macV+O9UzmSWmUr6CKf+dMtysDeC0PnG3y6kBkRO08BJx52g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.73.0': - resolution: {integrity: sha512-QrC1S5VL1+N5rvorB15jH7CPvjVHA7GyUI5oVEd8Bd9m5605RDBQYTxDQ920DuylRoZOh2V7/xXpfCtFUrojUQ==} + '@oxfmt/binding-linux-s390x-gnu@0.74.0': + resolution: {integrity: sha512-6X7BbZNz34+OHM6Oqjf6TRcUyRn6aRcB4VtIDZAF/uQRyeVhg+G2hv6MlMvmnRekVSyTJ/8rG/u8W2dRFW6glg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.73.0': - resolution: {integrity: sha512-dFcUKQPltCEPa+YlkF3zqufn6nJPZdG73LpnjBvZFFsU3laCMssDXSYB99mVaj43FqdEgjpz5TO6gbYXcczd9g==} + '@oxfmt/binding-linux-x64-gnu@0.74.0': + resolution: {integrity: sha512-4kYIQK53yJXrxwxSzx8H2bojlf6/KezLGYLRXzV9QCCkoiCsvBTCid9Bg83dUPTLPa3g9K0730f6MKDbck1+OA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.73.0': - resolution: {integrity: sha512-LmQe+nV8e+eUwfPNCxDF+owbsg88toBiR/8rZRgVOeKQP3MaGefXZRvBoc1UG37iWEQ/9JlCXG9Mc4nf4N3agw==} + '@oxfmt/binding-linux-x64-musl@0.74.0': + resolution: {integrity: sha512-MfWz4wDREjp/JQDmuxtKieix4FHGa1z5rBvB3betdcPwZvf5fqeQrlCm1mmoyuUVQOh9Pt3M61HwN94kJCxw7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.73.0': - resolution: {integrity: sha512-8weOr1OKknE0G2DbR6ejJUT9AOsoBU6cXlsetmbihHv1K+bK7qP61T6lJxY0k/aAO7GyOJiRGNlL+04r4eH8Xg==} + '@oxfmt/binding-openharmony-arm64@0.74.0': + resolution: {integrity: sha512-6rmAApkq5s1kbDjvNI6Nl5A+JqQkLORb2sR941pf5HWsrd01WGKyagK/B4vX9+kh1FfvRkNgEu59FHvVk4H8cQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.73.0': - resolution: {integrity: sha512-mZV+QxXYgb8E18gzAFItb937VIsE4dqWdStrD3tRRC52UubOYEuU+e7NaIqUsGdB5YsJqNeuck3Ue/af2r9wyA==} + '@oxfmt/binding-win32-arm64-msvc@0.74.0': + resolution: {integrity: sha512-+5SpRUnZNGeXTyumHnpHa791tQtt04qZs076WT8eTcGF2OJ631VLnf7S0kQSPuDG+PtJM0phadEB2WAr5n1w9g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.73.0': - resolution: {integrity: sha512-QJPUsbgUb97ZPPMkFUq6VQq0fLhC4Np19KL9aB/qKVwXnpKE6MIiILXqzhKnQ/UbnEDjKJ7Xv1d6zH4bYPCUdQ==} + '@oxfmt/binding-win32-ia32-msvc@0.74.0': + resolution: {integrity: sha512-1Fg+wYBO3Dsti1QX+/6EYHjeQzLC84tLOAqPu/r7ah3UJlUs9C/DB703Ay9Vcg+8VMzqZA2P5VGhzJraVUD4OQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.73.0': - resolution: {integrity: sha512-cGcze2fu+ykIaFtkEvZlkZ3HvnCLk+WISthtq99zWTWn0x13t5L9TxvGdLZMUMvtfYR+ICkiCwKnguVubdcw0w==} + '@oxfmt/binding-win32-x64-msvc@0.74.0': + resolution: {integrity: sha512-y9jXUgPBm2TZIWbQhMYRRYAQM57+r4fbykgc+j24ICIIZl+pcuoNhnq1CRGm3oFiHnEUnZO00qOtSPGQdJ2ziQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -629,130 +629,130 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.88.0': - resolution: {integrity: sha512-k6Fgkf86ubfeH4GvqYsRolfuVAp0pfYVZvifSlXNHPdMaWUOfxUrmLEeDD7hF6U2oJuhsVxhSWq86/xCJ+9MmA==} + '@oxlint/binding-android-arm-eabi@1.89.0': + resolution: {integrity: sha512-Pflpba5F3gRAQ12sWglRxvWTHEdAusNDvmi4Y5N1T5SY1AGkM7AixOnET8qkrnLJ/F32qAlNokO6eCknT8tmnw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.88.0': - resolution: {integrity: sha512-AsIyhl9o1ejwtac7NMbhKpMdSml6NWR0ljKjZERxC8OHYDAW8o/Om9n1QHVLi4VCEUo42Zte2/Op9kyoGpJZMg==} + '@oxlint/binding-android-arm64@1.89.0': + resolution: {integrity: sha512-dYmyD1x7LaPlb79vZWUcpTShk8dI9z1I+IubtqrBxWNJaDhQdPHwcsvg7q5StTU4xWyd9AwPfLaTYFZY8NZgtQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.88.0': - resolution: {integrity: sha512-qgBjB7eEKcCB4RqjXGuoSZq74g0wD09fWFJ+HWcCJcKaga8TeUHtb+BY0SsxdleSLBgx4RZ7qmgo0EzqEq7JoQ==} + '@oxlint/binding-darwin-arm64@1.89.0': + resolution: {integrity: sha512-Z/oxOPiIqiS7fItRdxaaOEC+6GD3OpGi0IxoR0dMefzQHolsKLzScw2DNcu/5gUPok3tv+G9mslvYB5bo3i0qg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.88.0': - resolution: {integrity: sha512-sCz3X5Mw/xdzEzn4fsCt1Iq8NKJnGNfhR+b9B5k+4DysYAHsmIhYoocnBPiQ9kvY3+pUun2A0Z8Vtun/Nv6YoQ==} + '@oxlint/binding-darwin-x64@1.89.0': + resolution: {integrity: sha512-eAjTkHSjAxrdCPJxO6y5sPQL5pYLKb2kvxZz9UU7tXnOZk9I/HLtkvx9S7GHY883k+AmL/eC7mqFt+w7WCwk0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.88.0': - resolution: {integrity: sha512-xTOdYOIqCufgIj0knvxu86FBfovP6QBiD7ngNva01gb8xPQCD5gXkE5APGczH35zStim1O3zqvFbLQmY85xXRg==} + '@oxlint/binding-freebsd-x64@1.89.0': + resolution: {integrity: sha512-0lNCA9Tl1n1FKpqy2/9ckp0kIuSrtJ/l9bMRgaEhOtM7TsOHshFuwOcvW8oA9teXLMOJT61LjEKWiaHDFGxF/w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.88.0': - resolution: {integrity: sha512-BLadtw88NdYJBrAtC1dYzrPfSP2d5zgx6iuZycdD0YmZBJ2Nqbit9Z4PSaIb6gxK25dweArGMPbOmq/0ugwUPg==} + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': + resolution: {integrity: sha512-OTFEvi9W+JczaZD1Caj17SGCoW8H7TE+fvx0D7ATEWa1Lohn91UMxFQ6r145SiGy/yCxeIwptMys/RAM7gSVSQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.88.0': - resolution: {integrity: sha512-wILSczl/DaZsCcf7j7tvNbssp8hxEHIJxrv4j7+JjI3TAL1U/JTWbeUtoJJC/vo8+2hkTmHLYoKJjmiNbijdXw==} + '@oxlint/binding-linux-arm-musleabihf@1.89.0': + resolution: {integrity: sha512-fRxgNHvBLswj6MrhW2uBq5UDhPRQlCbHeq/Nf8yuG9zOxhDsXqX/mi2PX1axgsQxMm88el7HLiJAzglAnflYxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.88.0': - resolution: {integrity: sha512-jutT2Hy8JCuZuMwIFHo2HtyxfOfBJHyQxlBHBK1SL9W2f8n/WhjAP0FnDMz9d/mjhv9+QA0mx5AdQ9fDjsrFFg==} + '@oxlint/binding-linux-arm64-gnu@1.89.0': + resolution: {integrity: sha512-v9ENzHvwrQoYSWTtviH1pOYc7R9UovDxVofnck50cNmPihRRTcrg5X0UlRbLOZ7IVtxPOlh2hb7kDO+FpE8SfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.88.0': - resolution: {integrity: sha512-8N8iGV9hh4Lkeuq7QsEkjoXNRRflqR1G2qyoGYXcA0k5qllrdqmfylKEXyHIVERxqNDyMO4StqDQ0uLxXcBQAg==} + '@oxlint/binding-linux-arm64-musl@1.89.0': + resolution: {integrity: sha512-vfffe8mQ484AzVadlUOFpgL+JDwhvs9hrzxTgQP1FVfi4ZEUUYeffXlzC9dFV7Dgnu5s/KrNKvfmG6HxBd/ISQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.88.0': - resolution: {integrity: sha512-5ydcQt3znNQ/rGEWYRvpS2cK9+T18P2VYgnyJ+eloEAwi9YUmsXy5VGrRqNWIK6qzWwHGnEE3ojVqqQNo+f0hA==} + '@oxlint/binding-linux-ppc64-gnu@1.89.0': + resolution: {integrity: sha512-Buonlp4hVS7RGNP5M7bPpGNQrenyzA2C64JcO/oz9C1c6dGOXJ7VEAnaiAMY7voou0JYyCJdvDVlFQc5DZX3DQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.88.0': - resolution: {integrity: sha512-iGx0uvFT54b/5DPJdEBCxYIrVB3v0uTy+JhRh3CyEp8/22BXDkG8rffkFwREdLsdj9mOrPbJYVOZw8ipcBuzjw==} + '@oxlint/binding-linux-riscv64-gnu@1.89.0': + resolution: {integrity: sha512-1eVwJJrJ3lYIPT+cm7BcBBZZXY6IQZP/oyonaruTor/MrVyWOpvnwt1oD5CLdsHg5aVBIxXodQSvt+1gK6ck5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.88.0': - resolution: {integrity: sha512-Wt47mKMWelVjRjuR7BCFxXkB0m79K1sTEbqMHRWciKUgo5iv7sWsF4RXSOyFYHD9zerNrkRZblnN273rhwOA1Q==} + '@oxlint/binding-linux-riscv64-musl@1.89.0': + resolution: {integrity: sha512-fRQQsKUGShdEL5Fl9o9uhj3grkZbeXKLv0icQ5tspUCNYSDy8wzzEcP1WC2bKdTQ0rSszbKlO3iiHORxV22DLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.88.0': - resolution: {integrity: sha512-g3L12KCS4+1gtUt3RbJ4F+Yxx4LNmmZLEPfhd8rAHEJjJk2lAgNfnjGoP/phJyhG5WS6VbNYbJtadJelNfvTMA==} + '@oxlint/binding-linux-s390x-gnu@1.89.0': + resolution: {integrity: sha512-dEtYjIy6+fHcjKklsmtmMkZRhwHr2UbzjmpvuPtQYSeN9awQ2hx5JLms5Wvri+8P/NZtD8v5hfgHALUql+/Ycg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.88.0': - resolution: {integrity: sha512-og/XHVDOD9ZJgRiZkT9DgzQd4sr+JZW7e4l17Q/jVMZiIbeEd4uIu9iAoUbTF1gIvVZoRc4QDOxyVUvQGp4WHg==} + '@oxlint/binding-linux-x64-gnu@1.89.0': + resolution: {integrity: sha512-hqjwIj5KOYM8Wjef/p7IruNi1iByLhSzbMBk62y22P/E8q7Jj0Pp+DeUTKTRYyZYCDtU1wEGlnWOnttHbyNIww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.88.0': - resolution: {integrity: sha512-EmhBsp+xuKb6YbZNEMLBo80Xw71AZOIm18do8ni/GLX/Lw1XzNhVYpbxO1KasYtoAvrGtCOdCPULRlIE0bPbTQ==} + '@oxlint/binding-linux-x64-musl@1.89.0': + resolution: {integrity: sha512-iAQN3e6o9UgUCcdGwqRx/jYcKbG3HK7ur3rH6lTqVcQeNGZ85AeWIg6cgn/edwFQ0ofEZZItMwkzyCVTahnvEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.88.0': - resolution: {integrity: sha512-DBUcx/KwYdg1DAz+G01P/GFv512J04EsZadbHptx2FTpWT/MWRg21ZToNHt9pnsfQRBQ3et/g+vqzbvrdAVbuA==} + '@oxlint/binding-openharmony-arm64@1.89.0': + resolution: {integrity: sha512-MNxjEByz3N/7Q4qFQuPWvc14TpN9LoD3pECEDsJTZbP/wyB7U+hduUXEl7QAbbgVezFwsb9sWtNUO6XVBndOhg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.88.0': - resolution: {integrity: sha512-xeVd0n4XG5HvynxfYvXi6uB+JvZbVZataowPS8cYpMbYg9mwE4sSyElFQd4LVJFifq8bRLDSR18XgEildmcXAg==} + '@oxlint/binding-win32-arm64-msvc@1.89.0': + resolution: {integrity: sha512-xxe+i9wfuFso/VnoL4mfj0UlzE+cfCFiUB06vkXCMyQdgVVmItF5FfAF4tATAktSU6uHruWR+FjqvAT9cHZZzg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.88.0': - resolution: {integrity: sha512-MA6iOw//amleaz2mVMsZrA7NHlEexjTrhIxSMFCXQi6IUuQO+3Od00GZhr2iCYw6RFzMW36jkKK/WHBXMsoQAw==} + '@oxlint/binding-win32-ia32-msvc@1.89.0': + resolution: {integrity: sha512-saFy07cO94E664glwgLmKLI40HgTL1PY86y4T6+ud4niX4H0Kdqjcmqmjps7mQNdBytV5guGbXDdyQtRfeLbLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.88.0': - resolution: {integrity: sha512-3Sz6rVWe8Dy+ufsXurFN1GX2dX43F/lQMSLelK9gFjiba7nHvLlqlhR5cHlWVnCpSqUY1hN9pyCX9XToe6ve6g==} + '@oxlint/binding-win32-x64-msvc@1.89.0': + resolution: {integrity: sha512-hLFJ/o0f/dTJh/EW0vmcbREYSQO9tYgfiAAlsOkiLFiJuRRWvYtFELUXN5o2ijpSo/tlO1n+YlWN58tZwPG18A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/plugins@1.88.0': - resolution: {integrity: sha512-G2xbfWTrbqYRhR/vfeKNMQf/sS3/ti0iKJAAkRxHuXWZH/hne/dyIb9QKk5LuR6cJLARxsL6FMoAYmfspd74MQ==} + '@oxlint/plugins@1.89.0': + resolution: {integrity: sha512-TJBBNl4KIxbxrSk7gsqlz6iM1kxidfyTizEzfWqXvijF2Ou8/Da58GvjemBVmCmaUamwRij+JwsVc0kpBFuqfg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@parcel/watcher-android-arm64@2.6.0': @@ -1867,12 +1867,9 @@ packages: resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} peerDependencies: msw: ^2.4.9 || ^3.0.0 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true - vite: - optional: true '@vitest/pretty-format@5.0.5': resolution: {integrity: sha512-24sSdIlMA9DVPXnYiB9siwU0bTd887Y9jX3+vabztCSsvHwo7ipvJ/a2L3Rljw8+2rK1f5fCj3h5U9I/Jsqgbw==} @@ -1891,14 +1888,14 @@ packages: '@vitest/utils@5.0.5': resolution: {integrity: sha512-/rBu7gzLDQI/SC79Tn98b5DwOgaHU/Gt4I9OVeP3YofzNaYtbImRCOJQmT1S6vOJg9gEIlWC+r3QxKkjuSa1nQ==} - '@voidzero-dev/vite-plus-core@1.3.0': - resolution: {integrity: sha512-Bk/LBwNrxoB8XbGUtGJE3r+U9P4puvdZ6UH36ekygE1J+on/ytwM/yBgQwBnvmX6/MVax+2xxj8wk0N/KkyNeA==} + '@voidzero-dev/vite-plus-core@1.4.0': + resolution: {integrity: sha512-LTGXCdZ58qJKfuCOmf0hnZOVA64eOgzDQOqYpI+71tPxk/0cGqPQhYDGQQZoEvNLAn1G2Lo4gKadsQdWxb99WQ==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 '@types/node': ^20.19.0 || >=22.12.0 '@typescript/native-preview': '*' - '@vitejs/devtools': ^0.7.1 + '@vitejs/devtools': ^0.7.1 || ^0.8.0 '@volar/typescript': ~2.4.0 '@vue/language-core': ~3.2.0 || ~3.3.0 esbuild: ^0.27.0 || ^0.28.0 @@ -1960,54 +1957,54 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@1.3.0': - resolution: {integrity: sha512-BF4W+UwGxUkHPrH2pZLhafJfkJT20WZIbMzZgZLGQ44LPpZPX8Sb+qtblUnXUkkCzTqad4XtSPgiLKglvZkNQQ==} + '@voidzero-dev/vite-plus-darwin-arm64@1.4.0': + resolution: {integrity: sha512-5Ie3vnrFrF5GFU+0VhbLuyoSmnzd8m42BvbLjncMOmAb6D1sEq4g37dlxPiVcJEqITM0t4zFnLkNSF4YAlk0FQ==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@1.3.0': - resolution: {integrity: sha512-vLZTk0r+s3e7mycj4SGBbcpK+xyM8a86hXQZtQbJyW7xrGJzsavISy7/m4gGZpWN32Hl4Tmx9V/ffeaoXLkRdg==} + '@voidzero-dev/vite-plus-darwin-x64@1.4.0': + resolution: {integrity: sha512-vaR7F+ix/91EycJYXOQ24Nzw470U12x3L+YLbbtuRwHDMW67fCbhXzcW+otZS8q4eRdIzEcA5svv9P8kcsaRkA==} engines: {node: '>=20.0.0'} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@1.3.0': - resolution: {integrity: sha512-H43Dj9RkEyjOFXxTk3DLTa6hqmHhxUN9vbbqNdwQoc3/TPNhC1zsLZy3qgUFbYGLdS7M9jICCqFzj5GDSYrhIw==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.4.0': + resolution: {integrity: sha512-58/PZrp+f3Hy6kFVKqbrZVenjo5t012rTOgRSgMoJyOhJjXBHqvORh9lB9GIip9r9maoWN31R1a44obPX9MD7g==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-arm64-musl@1.3.0': - resolution: {integrity: sha512-Se2GlvzUZOvFy+5hoQHoG7LFeiplKs+EJwaskayCSLwZAP+e6/LTK7EK+8OcdMpmdsLPtZoTJHkIjSzrQowJwQ==} + '@voidzero-dev/vite-plus-linux-arm64-musl@1.4.0': + resolution: {integrity: sha512-Ij08vPrxG1J23wQVm0LtLPrQevv4tos8TirP/jGTpQQmQAgYOpmIfMrx8BF+vOGMncOIo+kKacp4jPVmhctfhg==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-linux-x64-gnu@1.3.0': - resolution: {integrity: sha512-moxojk6zmog2SaLE4/cG75acE6X3cjR0/52Ba8OG2ct0r+dv55VSE0kellAuxS6o9VHy7TBhkIpwpFpQ34rnAA==} + '@voidzero-dev/vite-plus-linux-x64-gnu@1.4.0': + resolution: {integrity: sha512-RbM21Y5cSx5WesaFV/Swo22JRvimjO6NDItoL5wNI6NTa987ck7v19HBmp+V4nkOCFHbMdxwfhyALDw0y06xEA==} engines: {node: '>=20.0.0'} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-musl@1.3.0': - resolution: {integrity: sha512-CykxH008Dy9u20Myki9/UIqq3Dbo1ZMqa098aS6AtWp3QKyEKzSLPB3jKDUklgmu83xiFe5BpPA+33oF0yNY7A==} + '@voidzero-dev/vite-plus-linux-x64-musl@1.4.0': + resolution: {integrity: sha512-Q+p+YJztEmqjJoumTmPaBQ/PEx3eFhiNTQTn5+85gkBUiRexkFQ8A86TMFxbKVgrJbiAOltkrJRmVRhRs5Q0sA==} engines: {node: '>=20.0.0'} cpu: [x64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-win32-arm64-msvc@1.3.0': - resolution: {integrity: sha512-l61S9b8aeiDpi8wPRfz4HonqRkLpt/OVYs0sCskvlXYSW/L8qfLOtuWH/xpF8xDjMIfBefKejBVfU1UlRax5Tw==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.4.0': + resolution: {integrity: sha512-PLGVov8c4/QfveOM/AYjYDlgMCQ0SxCmJp392qBgxaSXP0nEg6yP4hn5UtnhxvluhHEzVkTf2r1Q3LNlZRI7kw==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@1.3.0': - resolution: {integrity: sha512-b6Ng8YezA6JA3adQBNA+8Mn26+OX4fw/tsPfnk//5Cx+1XFDZoAxWYXtGzc0yUQPEKnoGtfw/0OUvcibg61yYg==} + '@voidzero-dev/vite-plus-win32-x64-msvc@1.4.0': + resolution: {integrity: sha512-DYG7wDsMLLs2/+8qfeyLScdoMh+aPM4IZanoVPSNt8fMeeXR2GQiiZDKc0qZHKN9+YcdCGK1HLAdMAGSywKvsA==} engines: {node: '>=20.0.0'} cpu: [x64] os: [win32] @@ -2904,8 +2901,8 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - oxfmt@0.73.0: - resolution: {integrity: sha512-Ke4xbpuzqIBP4bkd48q815RhqWNJqOBzxVA9199M4qcF0y0LtpUv7UTg1dJ5ea6jw0RBUDZvX3qOUMNmmxfxbw==} + oxfmt@0.74.0: + resolution: {integrity: sha512-G4OP7FgwGYZR5ttRpCOOfJdyRsSyvtn34vSXnjDq6Oqed5Dufg5mB44mank+51nAdHOKuTJ0lj0mksY7L+N61A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2924,8 +2921,8 @@ packages: resolution: {integrity: sha512-vQ9WDF3fiZJvRdSDosNOflerOCOpwol6vWzVDxW76RcU19cjTh7engvkHLvmv+tzedR8fAiJUVUGr6IVZdkzXQ==} hasBin: true - oxlint@1.88.0: - resolution: {integrity: sha512-uIcFgw8BmqvFRGzH5+r7d2zJMEexwNSrEmM6WQNnGh3VkO0XYTEFoyxyNE6tjKMUib9Cue4XJA2DoWruO5D6aw==} + oxlint@1.89.0: + resolution: {integrity: sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3352,8 +3349,8 @@ packages: vite-plugin-full-reload@1.2.0: resolution: {integrity: sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==} - vite-plus@1.3.0: - resolution: {integrity: sha512-wZGsDeYfPcrCegqgY68pJJHGTpEZdsZvIMH2/mWXkGkBD72UY3M7z24Sv3ZeOL/FrqbbtyqBOfkKO6BvoQ8wMw==} + vite-plus@1.4.0: + resolution: {integrity: sha512-bLyUKNFl0XjbHe20FpDM6OuknOb2E4ueoWKCW4n+wMO2+lQtA9tmAjW+Xs0Xe9rg2+tCgY3W13GOgDh2wjwjAQ==} engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} hasBin: true peerDependencies: @@ -3613,65 +3610,65 @@ snapshots: '@one-ini/wasm@0.2.1': {} - '@oxc-project/runtime@0.154.0': {} + '@oxc-project/runtime@0.155.0': {} - '@oxc-project/types@0.154.0': {} + '@oxc-project/types@0.155.0': {} - '@oxfmt/binding-android-arm-eabi@0.73.0': + '@oxfmt/binding-android-arm-eabi@0.74.0': optional: true - '@oxfmt/binding-android-arm64@0.73.0': + '@oxfmt/binding-android-arm64@0.74.0': optional: true - '@oxfmt/binding-darwin-arm64@0.73.0': + '@oxfmt/binding-darwin-arm64@0.74.0': optional: true - '@oxfmt/binding-darwin-x64@0.73.0': + '@oxfmt/binding-darwin-x64@0.74.0': optional: true - '@oxfmt/binding-freebsd-x64@0.73.0': + '@oxfmt/binding-freebsd-x64@0.74.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.73.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.74.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.73.0': + '@oxfmt/binding-linux-arm-musleabihf@0.74.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.73.0': + '@oxfmt/binding-linux-arm64-gnu@0.74.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.73.0': + '@oxfmt/binding-linux-arm64-musl@0.74.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.73.0': + '@oxfmt/binding-linux-ppc64-gnu@0.74.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.73.0': + '@oxfmt/binding-linux-riscv64-gnu@0.74.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.73.0': + '@oxfmt/binding-linux-riscv64-musl@0.74.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.73.0': + '@oxfmt/binding-linux-s390x-gnu@0.74.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.73.0': + '@oxfmt/binding-linux-x64-gnu@0.74.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.73.0': + '@oxfmt/binding-linux-x64-musl@0.74.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.73.0': + '@oxfmt/binding-openharmony-arm64@0.74.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.73.0': + '@oxfmt/binding-win32-arm64-msvc@0.74.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.73.0': + '@oxfmt/binding-win32-ia32-msvc@0.74.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.73.0': + '@oxfmt/binding-win32-x64-msvc@0.74.0': optional: true '@oxlint-tsgolint/darwin-arm64@7.0.2004': @@ -3692,64 +3689,64 @@ snapshots: '@oxlint-tsgolint/win32-x64@7.0.2004': optional: true - '@oxlint/binding-android-arm-eabi@1.88.0': + '@oxlint/binding-android-arm-eabi@1.89.0': optional: true - '@oxlint/binding-android-arm64@1.88.0': + '@oxlint/binding-android-arm64@1.89.0': optional: true - '@oxlint/binding-darwin-arm64@1.88.0': + '@oxlint/binding-darwin-arm64@1.89.0': optional: true - '@oxlint/binding-darwin-x64@1.88.0': + '@oxlint/binding-darwin-x64@1.89.0': optional: true - '@oxlint/binding-freebsd-x64@1.88.0': + '@oxlint/binding-freebsd-x64@1.89.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.88.0': + '@oxlint/binding-linux-arm-gnueabihf@1.89.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.88.0': + '@oxlint/binding-linux-arm-musleabihf@1.89.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.88.0': + '@oxlint/binding-linux-arm64-gnu@1.89.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.88.0': + '@oxlint/binding-linux-arm64-musl@1.89.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.88.0': + '@oxlint/binding-linux-ppc64-gnu@1.89.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.88.0': + '@oxlint/binding-linux-riscv64-gnu@1.89.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.88.0': + '@oxlint/binding-linux-riscv64-musl@1.89.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.88.0': + '@oxlint/binding-linux-s390x-gnu@1.89.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.88.0': + '@oxlint/binding-linux-x64-gnu@1.89.0': optional: true - '@oxlint/binding-linux-x64-musl@1.88.0': + '@oxlint/binding-linux-x64-musl@1.89.0': optional: true - '@oxlint/binding-openharmony-arm64@1.88.0': + '@oxlint/binding-openharmony-arm64@1.89.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.88.0': + '@oxlint/binding-win32-arm64-msvc@1.89.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.88.0': + '@oxlint/binding-win32-ia32-msvc@1.89.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.88.0': + '@oxlint/binding-win32-x64-msvc@1.89.0': optional: true - '@oxlint/plugins@1.88.0': {} + '@oxlint/plugins@1.89.0': {} '@parcel/watcher-android-arm64@2.6.0': optional: true @@ -4513,12 +4510,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))': + '@tailwindcss/vite@4.3.3(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' '@testing-library/dom@10.4.2': dependencies: @@ -4628,94 +4625,216 @@ snapshots: '@ungap/structured-clone@1.4.0': {} - '@vitejs/plugin-react@6.1.2(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))': + '@vitejs/plugin-react@6.1.2(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' - '@vitejs/plugin-vue@6.0.10(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vue@3.5.44(typescript@7.0.2))': + '@vitejs/plugin-vue@6.0.10(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vue@3.5.44(typescript@7.0.2))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' vue: 3.5.44(typescript@7.0.2) - '@vitest/browser-preview@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5)': + '@vitest/browser-preview@5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5)': dependencies: '@testing-library/dom': 10.4.2 '@testing-library/user-event': 14.6.8(@testing-library/dom@10.4.2) - '@vitest/browser': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5) - vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3) + '@vitest/browser': 5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5) + vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' - bufferutil + - esbuild + - jiti + - less - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun - utf-8-validate - - vite + - vue-tsc + - yaml - '@vitest/browser-preview@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5)': + '@vitest/browser-preview@5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5)': dependencies: '@testing-library/dom': 10.4.2 '@testing-library/user-event': 14.6.8(@testing-library/dom@10.4.2) - '@vitest/browser': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5) - vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3) + '@vitest/browser': 5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5) + vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' - bufferutil + - esbuild + - jiti + - less - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun - utf-8-validate - - vite + - vue-tsc + - yaml - '@vitest/browser@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5)': + '@vitest/browser@5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5)': dependencies: '@blazediff/core': 1.10.0 - '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + '@vitest/mocker': 5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) '@vitest/ui': 5.0.5(vitest@5.0.5) '@vitest/utils': 5.0.5 magic-string: 1.4.3 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.2.0 - vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3) + vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) ws: 8.22.0 transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' - bufferutil + - esbuild + - jiti + - less - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun - utf-8-validate - - vite + - vue-tsc + - yaml - '@vitest/browser@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5)': + '@vitest/browser@5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5)': dependencies: '@blazediff/core': 1.10.0 - '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + '@vitest/mocker': 5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) '@vitest/ui': 5.0.5(vitest@5.0.5) '@vitest/utils': 5.0.5 magic-string: 1.4.3 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.2.0 - vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3) + vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) ws: 8.22.0 transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' - bufferutil + - esbuild + - jiti + - less - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun - utf-8-validate - - vite + - vue-tsc + - yaml - '@vitest/mocker@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))': + '@vitest/mocker@5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)': dependencies: '@jridgewell/trace-mapping': 0.3.31 '@vitest/spy': 5.0.5 estree-walker: 3.0.3 magic-string: 1.4.3 - optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' + - esbuild + - jiti + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - vue-tsc + - yaml - '@vitest/mocker@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))': + '@vitest/mocker@5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)': dependencies: '@jridgewell/trace-mapping': 0.3.31 '@vitest/spy': 5.0.5 estree-walker: 3.0.3 magic-string: 1.4.3 - optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@types/node' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' + - esbuild + - jiti + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - vue-tsc + - yaml '@vitest/pretty-format@5.0.5': dependencies: @@ -4738,7 +4857,7 @@ snapshots: pathe: 2.0.3 sirv: 3.0.2 tinyrainbow: 3.2.0 - vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3) + vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) '@vitest/utils@5.0.5': dependencies: @@ -4746,74 +4865,74 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.2.0 - '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)': + '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)': dependencies: - '@oxc-project/runtime': 0.154.0 - '@oxc-project/types': 0.154.0 + '@oxc-project/runtime': 0.155.0 + '@oxc-project/types': 0.155.0 lightningcss: 1.33.0 postcss: 8.5.31 yuku-parser: 0.19.0 optionalDependencies: '@types/node': 22.20.5 - '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0 - '@voidzero-dev/vite-plus-darwin-x64': 1.3.0 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0 - '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0 - '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0 - '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0 - '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0 + '@voidzero-dev/vite-plus-darwin-arm64': 1.4.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.4.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.4.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.4.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.4.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.4.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.4.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.4.0 fsevents: 2.3.3 jiti: 1.21.7 sass: 1.106.0 terser: 5.51.2 typescript: 7.0.2 - '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)': + '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)': dependencies: - '@oxc-project/runtime': 0.154.0 - '@oxc-project/types': 0.154.0 + '@oxc-project/runtime': 0.155.0 + '@oxc-project/types': 0.155.0 lightningcss: 1.33.0 postcss: 8.5.31 yuku-parser: 0.19.0 optionalDependencies: '@types/node': 22.20.5 - '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0 - '@voidzero-dev/vite-plus-darwin-x64': 1.3.0 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0 - '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0 - '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0 - '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0 - '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0 + '@voidzero-dev/vite-plus-darwin-arm64': 1.4.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.4.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.4.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.4.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.4.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.4.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.4.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.4.0 fsevents: 2.3.3 jiti: 2.7.0 sass: 1.106.0 terser: 5.51.2 typescript: 7.0.2 - '@voidzero-dev/vite-plus-darwin-arm64@1.3.0': + '@voidzero-dev/vite-plus-darwin-arm64@1.4.0': optional: true - '@voidzero-dev/vite-plus-darwin-x64@1.3.0': + '@voidzero-dev/vite-plus-darwin-x64@1.4.0': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@1.3.0': + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.4.0': optional: true - '@voidzero-dev/vite-plus-linux-arm64-musl@1.3.0': + '@voidzero-dev/vite-plus-linux-arm64-musl@1.4.0': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@1.3.0': + '@voidzero-dev/vite-plus-linux-x64-gnu@1.4.0': optional: true - '@voidzero-dev/vite-plus-linux-x64-musl@1.3.0': + '@voidzero-dev/vite-plus-linux-x64-musl@1.4.0': optional: true - '@voidzero-dev/vite-plus-win32-arm64-msvc@1.3.0': + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.4.0': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@1.3.0': + '@voidzero-dev/vite-plus-win32-x64-msvc@1.4.0': optional: true '@vue/compiler-core@3.5.44': @@ -5394,18 +5513,18 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' - laravel-vite-plugin@3.2.0(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): + laravel-vite-plugin@3.2.0(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): dependencies: picocolors: 1.1.1 tinyglobby: 0.2.17 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' vite-plugin-full-reload: 1.2.0 - laravel-vite-plugin@3.2.0(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): + laravel-vite-plugin@3.2.0(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): dependencies: picocolors: 1.1.1 tinyglobby: 0.2.17 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' vite-plugin-full-reload: 1.2.0 lightningcss-android-arm64@1.32.0: @@ -5634,55 +5753,55 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - oxfmt@0.73.0(vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): + oxfmt@0.74.0(vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): dependencies: tinypool: 2.2.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.73.0 - '@oxfmt/binding-android-arm64': 0.73.0 - '@oxfmt/binding-darwin-arm64': 0.73.0 - '@oxfmt/binding-darwin-x64': 0.73.0 - '@oxfmt/binding-freebsd-x64': 0.73.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.73.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.73.0 - '@oxfmt/binding-linux-arm64-gnu': 0.73.0 - '@oxfmt/binding-linux-arm64-musl': 0.73.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.73.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.73.0 - '@oxfmt/binding-linux-riscv64-musl': 0.73.0 - '@oxfmt/binding-linux-s390x-gnu': 0.73.0 - '@oxfmt/binding-linux-x64-gnu': 0.73.0 - '@oxfmt/binding-linux-x64-musl': 0.73.0 - '@oxfmt/binding-openharmony-arm64': 0.73.0 - '@oxfmt/binding-win32-arm64-msvc': 0.73.0 - '@oxfmt/binding-win32-ia32-msvc': 0.73.0 - '@oxfmt/binding-win32-x64-msvc': 0.73.0 - vite-plus: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) - - oxfmt@0.73.0(vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): + '@oxfmt/binding-android-arm-eabi': 0.74.0 + '@oxfmt/binding-android-arm64': 0.74.0 + '@oxfmt/binding-darwin-arm64': 0.74.0 + '@oxfmt/binding-darwin-x64': 0.74.0 + '@oxfmt/binding-freebsd-x64': 0.74.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.74.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.74.0 + '@oxfmt/binding-linux-arm64-gnu': 0.74.0 + '@oxfmt/binding-linux-arm64-musl': 0.74.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.74.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.74.0 + '@oxfmt/binding-linux-riscv64-musl': 0.74.0 + '@oxfmt/binding-linux-s390x-gnu': 0.74.0 + '@oxfmt/binding-linux-x64-gnu': 0.74.0 + '@oxfmt/binding-linux-x64-musl': 0.74.0 + '@oxfmt/binding-openharmony-arm64': 0.74.0 + '@oxfmt/binding-win32-arm64-msvc': 0.74.0 + '@oxfmt/binding-win32-ia32-msvc': 0.74.0 + '@oxfmt/binding-win32-x64-msvc': 0.74.0 + vite-plus: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + + oxfmt@0.74.0(vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): dependencies: tinypool: 2.2.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.73.0 - '@oxfmt/binding-android-arm64': 0.73.0 - '@oxfmt/binding-darwin-arm64': 0.73.0 - '@oxfmt/binding-darwin-x64': 0.73.0 - '@oxfmt/binding-freebsd-x64': 0.73.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.73.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.73.0 - '@oxfmt/binding-linux-arm64-gnu': 0.73.0 - '@oxfmt/binding-linux-arm64-musl': 0.73.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.73.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.73.0 - '@oxfmt/binding-linux-riscv64-musl': 0.73.0 - '@oxfmt/binding-linux-s390x-gnu': 0.73.0 - '@oxfmt/binding-linux-x64-gnu': 0.73.0 - '@oxfmt/binding-linux-x64-musl': 0.73.0 - '@oxfmt/binding-openharmony-arm64': 0.73.0 - '@oxfmt/binding-win32-arm64-msvc': 0.73.0 - '@oxfmt/binding-win32-ia32-msvc': 0.73.0 - '@oxfmt/binding-win32-x64-msvc': 0.73.0 - vite-plus: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + '@oxfmt/binding-android-arm-eabi': 0.74.0 + '@oxfmt/binding-android-arm64': 0.74.0 + '@oxfmt/binding-darwin-arm64': 0.74.0 + '@oxfmt/binding-darwin-x64': 0.74.0 + '@oxfmt/binding-freebsd-x64': 0.74.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.74.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.74.0 + '@oxfmt/binding-linux-arm64-gnu': 0.74.0 + '@oxfmt/binding-linux-arm64-musl': 0.74.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.74.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.74.0 + '@oxfmt/binding-linux-riscv64-musl': 0.74.0 + '@oxfmt/binding-linux-s390x-gnu': 0.74.0 + '@oxfmt/binding-linux-x64-gnu': 0.74.0 + '@oxfmt/binding-linux-x64-musl': 0.74.0 + '@oxfmt/binding-openharmony-arm64': 0.74.0 + '@oxfmt/binding-win32-arm64-msvc': 0.74.0 + '@oxfmt/binding-win32-ia32-msvc': 0.74.0 + '@oxfmt/binding-win32-x64-msvc': 0.74.0 + vite-plus: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) oxlint-tsgolint@7.0.2004: optionalDependencies: @@ -5693,53 +5812,53 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2004 '@oxlint-tsgolint/win32-x64': 7.0.2004 - oxlint@1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): + oxlint@1.89.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.88.0 - '@oxlint/binding-android-arm64': 1.88.0 - '@oxlint/binding-darwin-arm64': 1.88.0 - '@oxlint/binding-darwin-x64': 1.88.0 - '@oxlint/binding-freebsd-x64': 1.88.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.88.0 - '@oxlint/binding-linux-arm-musleabihf': 1.88.0 - '@oxlint/binding-linux-arm64-gnu': 1.88.0 - '@oxlint/binding-linux-arm64-musl': 1.88.0 - '@oxlint/binding-linux-ppc64-gnu': 1.88.0 - '@oxlint/binding-linux-riscv64-gnu': 1.88.0 - '@oxlint/binding-linux-riscv64-musl': 1.88.0 - '@oxlint/binding-linux-s390x-gnu': 1.88.0 - '@oxlint/binding-linux-x64-gnu': 1.88.0 - '@oxlint/binding-linux-x64-musl': 1.88.0 - '@oxlint/binding-openharmony-arm64': 1.88.0 - '@oxlint/binding-win32-arm64-msvc': 1.88.0 - '@oxlint/binding-win32-ia32-msvc': 1.88.0 - '@oxlint/binding-win32-x64-msvc': 1.88.0 + '@oxlint/binding-android-arm-eabi': 1.89.0 + '@oxlint/binding-android-arm64': 1.89.0 + '@oxlint/binding-darwin-arm64': 1.89.0 + '@oxlint/binding-darwin-x64': 1.89.0 + '@oxlint/binding-freebsd-x64': 1.89.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.89.0 + '@oxlint/binding-linux-arm-musleabihf': 1.89.0 + '@oxlint/binding-linux-arm64-gnu': 1.89.0 + '@oxlint/binding-linux-arm64-musl': 1.89.0 + '@oxlint/binding-linux-ppc64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-musl': 1.89.0 + '@oxlint/binding-linux-s390x-gnu': 1.89.0 + '@oxlint/binding-linux-x64-gnu': 1.89.0 + '@oxlint/binding-linux-x64-musl': 1.89.0 + '@oxlint/binding-openharmony-arm64': 1.89.0 + '@oxlint/binding-win32-arm64-msvc': 1.89.0 + '@oxlint/binding-win32-ia32-msvc': 1.89.0 + '@oxlint/binding-win32-x64-msvc': 1.89.0 oxlint-tsgolint: 7.0.2004 - vite-plus: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + vite-plus: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) - oxlint@1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): + oxlint@1.89.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.88.0 - '@oxlint/binding-android-arm64': 1.88.0 - '@oxlint/binding-darwin-arm64': 1.88.0 - '@oxlint/binding-darwin-x64': 1.88.0 - '@oxlint/binding-freebsd-x64': 1.88.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.88.0 - '@oxlint/binding-linux-arm-musleabihf': 1.88.0 - '@oxlint/binding-linux-arm64-gnu': 1.88.0 - '@oxlint/binding-linux-arm64-musl': 1.88.0 - '@oxlint/binding-linux-ppc64-gnu': 1.88.0 - '@oxlint/binding-linux-riscv64-gnu': 1.88.0 - '@oxlint/binding-linux-riscv64-musl': 1.88.0 - '@oxlint/binding-linux-s390x-gnu': 1.88.0 - '@oxlint/binding-linux-x64-gnu': 1.88.0 - '@oxlint/binding-linux-x64-musl': 1.88.0 - '@oxlint/binding-openharmony-arm64': 1.88.0 - '@oxlint/binding-win32-arm64-msvc': 1.88.0 - '@oxlint/binding-win32-ia32-msvc': 1.88.0 - '@oxlint/binding-win32-x64-msvc': 1.88.0 + '@oxlint/binding-android-arm-eabi': 1.89.0 + '@oxlint/binding-android-arm64': 1.89.0 + '@oxlint/binding-darwin-arm64': 1.89.0 + '@oxlint/binding-darwin-x64': 1.89.0 + '@oxlint/binding-freebsd-x64': 1.89.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.89.0 + '@oxlint/binding-linux-arm-musleabihf': 1.89.0 + '@oxlint/binding-linux-arm64-gnu': 1.89.0 + '@oxlint/binding-linux-arm64-musl': 1.89.0 + '@oxlint/binding-linux-ppc64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-gnu': 1.89.0 + '@oxlint/binding-linux-riscv64-musl': 1.89.0 + '@oxlint/binding-linux-s390x-gnu': 1.89.0 + '@oxlint/binding-linux-x64-gnu': 1.89.0 + '@oxlint/binding-linux-x64-musl': 1.89.0 + '@oxlint/binding-openharmony-arm64': 1.89.0 + '@oxlint/binding-win32-arm64-msvc': 1.89.0 + '@oxlint/binding-win32-ia32-msvc': 1.89.0 + '@oxlint/binding-win32-x64-msvc': 1.89.0 oxlint-tsgolint: 7.0.2004 - vite-plus: 1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) + vite-plus: 1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) parse5@8.0.1: dependencies: @@ -6199,31 +6318,32 @@ snapshots: picocolors: 1.1.1 picomatch: 2.3.2 - vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2): + vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2): dependencies: - '@oxc-project/types': 0.154.0 - '@oxlint/plugins': 1.88.0 - '@vitest/browser': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5) - '@vitest/browser-preview': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5) - '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + '@oxc-project/runtime': 0.155.0 + '@oxc-project/types': 0.155.0 + '@oxlint/plugins': 1.89.0 + '@vitest/browser': 5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5) + '@vitest/browser-preview': 5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5) + '@vitest/mocker': 5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) '@vitest/pretty-format': 5.0.5 '@vitest/snapshot': 5.0.5 '@vitest/spy': 5.0.5 '@vitest/utils': 5.0.5 - oxfmt: 0.73.0(vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) - oxlint: 1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + oxfmt: 0.74.0(vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + oxlint: 1.89.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) oxlint-tsgolint: 7.0.2004 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' - vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3) + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0 - '@voidzero-dev/vite-plus-darwin-x64': 1.3.0 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0 - '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0 - '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0 - '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0 - '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0 + '@voidzero-dev/vite-plus-darwin-arm64': 1.4.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.4.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.4.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.4.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.4.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.4.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.4.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.4.0 transitivePeerDependencies: - '@arethetypeswrong/core' - '@astrojs/compiler-rs' @@ -6259,31 +6379,32 @@ snapshots: - vue-tsc - yaml - vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2): + vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2): dependencies: - '@oxc-project/types': 0.154.0 - '@oxlint/plugins': 1.88.0 - '@vitest/browser': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5) - '@vitest/browser-preview': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5) - '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + '@oxc-project/runtime': 0.155.0 + '@oxc-project/types': 0.155.0 + '@oxlint/plugins': 1.89.0 + '@vitest/browser': 5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5) + '@vitest/browser-preview': 5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5) + '@vitest/mocker': 5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) '@vitest/pretty-format': 5.0.5 '@vitest/snapshot': 5.0.5 '@vitest/spy': 5.0.5 '@vitest/utils': 5.0.5 - oxfmt: 0.73.0(vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) - oxlint: 1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + oxfmt: 0.74.0(vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + oxlint: 1.89.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.4.0(@types/node@22.20.5)(@vitest/ui@5.0.5)(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) oxlint-tsgolint: 7.0.2004 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' - vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3) + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vitest: 5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0 - '@voidzero-dev/vite-plus-darwin-x64': 1.3.0 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0 - '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0 - '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0 - '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0 - '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0 + '@voidzero-dev/vite-plus-darwin-arm64': 1.4.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.4.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.4.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.4.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.4.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.4.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.4.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.4.0 transitivePeerDependencies: - '@arethetypeswrong/core' - '@astrojs/compiler-rs' @@ -6319,10 +6440,10 @@ snapshots: - vue-tsc - yaml - vitest@5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3): + vitest@5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@1.21.7)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + '@vitest/mocker': 5.0.5(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -6333,21 +6454,41 @@ snapshots: tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@1.21.7)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 22.20.5 - '@vitest/browser-preview': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5) + '@vitest/browser-preview': 5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5) '@vitest/ui': 5.0.5(vitest@5.0.5) happy-dom: 21.0.4 jsdom: 30.1.3 transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' + - esbuild + - jiti + - less - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - vue-tsc + - yaml - vitest@5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jsdom@30.1.3): + vitest@5.0.5(@types/node@22.20.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(happy-dom@21.0.4)(jiti@2.7.0)(jsdom@30.1.3)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)) + '@vitest/mocker': 5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2) chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -6358,16 +6499,36 @@ snapshots: tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' + vite: '@voidzero-dev/vite-plus-core@1.4.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)' why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 22.20.5 - '@vitest/browser-preview': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2))(vitest@5.0.5) + '@vitest/browser-preview': 5.0.5(@types/node@22.20.5)(jiti@2.7.0)(sass@1.106.0)(terser@5.51.2)(typescript@7.0.2)(vitest@5.0.5) '@vitest/ui': 5.0.5(vitest@5.0.5) happy-dom: 21.0.4 jsdom: 30.1.3 transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@typescript/native-preview' + - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' + - esbuild + - jiti + - less - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - vue-tsc + - yaml vue-component-type-helpers@3.3.13: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9ad013edd8..ad8e8b9649 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,7 +14,7 @@ allowBuilds: # report a peer version mismatch despite using the bundled Vite runtime. # Update both core aliases with the vite-plus catalog pin; match Vitest to `vp toolchain vitest`. overrides: - 'vite@*': npm:@voidzero-dev/vite-plus-core@1.3.0 + 'vite@*': npm:@voidzero-dev/vite-plus-core@1.4.0 'vitest@*': 5.0.5 # Supply-chain defense: require packages to be at least 7 days old before @@ -39,8 +39,8 @@ catalog: sass: ^1.106.0 tailwindcss: ^4.3.3 typescript: ^7.0.2 - vite: npm:@voidzero-dev/vite-plus-core@1.3.0 - vite-plus: 1.3.0 + vite: npm:@voidzero-dev/vite-plus-core@1.4.0 + vite-plus: 1.4.0 vue: ^3.5.44 vue-json-pretty: ^2.6.0 vue-router: ^4.6.4 diff --git a/src/foundation/resources/exceptions/renderer/dist/scripts.js b/src/foundation/resources/exceptions/renderer/dist/scripts.js index 961d7803f8..b8ca1d2f81 100644 --- a/src/foundation/resources/exceptions/renderer/dist/scripts.js +++ b/src/foundation/resources/exceptions/renderer/dist/scripts.js @@ -2,7 +2,7 @@ var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],en ${n?`Expression: "`+n+`" -`:``}`,t),setTimeout(()=>{throw e},0)}var Re=!0;function ze(e){let t=Re;Re=!1;let n=e();return Re=t,n}function Be(e,t,n={}){let r;return F(e,t)(e=>r=e,n),r}function F(...e){return Ve(...e)}var Ve=()=>{};function He(e){Ve=e}var Ue;function We(e){Ue=e}function Ge(e,t){let n={};je(n,e);let r=[n,...be(e)],i=typeof t==`function`?Ke(r,t):Ye(r,t,e);return Ne.bind(null,e,t,i)}function Ke(e,t){return(n=()=>{},{scope:r={},params:i=[],context:a}={})=>{Re?Xe(n,t.apply(xe([r,...e]),i)):Xe(n,t,xe([r,...e]),i)}}var qe={};function Je(e,t){if(qe[e])return qe[e];let n=Object.getPrototypeOf(async function(){}).constructor,r=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,i=(()=>{try{let t=new n([`__self`,`scope`],`with (scope) { __self.result = ${r} }; __self.finished = true; return __self.result;`);return Object.defineProperty(t,"name",{value:`[Alpine] ${e}`}),t}catch(n){return Pe(n,t,e),Promise.resolve()}})();return qe[e]=i,i}function Ye(e,t,n){let r=Je(t,n);return(i=()=>{},{scope:a={},params:o=[],context:s}={})=>{r.result=void 0,r.finished=!1;let c=xe([a,...e]);if(typeof r==`function`){let e=r.call(s,r,c).catch(e=>Pe(e,n,t));r.finished?(Xe(i,r.result,c,o,n),r.result=void 0):e.then(e=>{Xe(i,e,c,o,n)}).catch(e=>Pe(e,n,t)).finally(()=>r.result=void 0)}}}function Xe(e,t,n,r,i){if(Re&&typeof t==`function`){let a=t.apply(n,r);a instanceof Promise?a.then(t=>Xe(e,t,n,r)).catch(e=>Pe(e,i,t)):e(a)}else typeof t==`object`&&t instanceof Promise?t.then(t=>e(t)):e(t)}function Ze(...e){return Ue(...e)}function Qe(e,t,n={}){let r={};je(r,e);let i=[r,...be(e)],a=xe([n.scope??{},...i]),o=n.params??[];if(t.includes(`await`)){let e=Object.getPrototypeOf(async function(){}).constructor;return new e([`scope`],`with (scope) { let __result = ${/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t}; return __result }`).call(n.context,a)}{let e=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(()=>{ ${t} })()`:t,r=Function([`scope`],`with (scope) { let __result = ${e}; return __result }`).call(n.context,a);return typeof r==`function`&&Re?r.apply(a,o):r}}var $e=`x-`;function et(e=``){return $e+e}function tt(e){$e=e}var nt={};function I(e,t){return nt[e]=t,{before(t){if(!nt[t]){console.warn(String.raw`Cannot find directive \`${t}\`. \`${e}\` will use the default order of execution`);return}let n=xt.indexOf(t);xt.splice(n>=0?n:xt.indexOf(`DEFAULT`),0,e)}}}function rt(e){return Object.keys(nt).includes(e)}function it(e,t,n){if(t=Array.from(t),e._x_virtualDirectives){let n=Object.entries(e._x_virtualDirectives).map(([e,t])=>({name:e,value:t})),r=at(n);n=n.map(e=>r.find(t=>t.name===e.name)?{name:`x-bind:${e.name}`,value:`"${e.value}"`}:e),t=t.concat(n)}let r={};return t.map(mt((e,t)=>r[e]=t)).filter(_t).map(yt(r,n)).sort(St).map(t=>dt(e,t))}function at(e){return Array.from(e).map(mt()).filter(e=>!_t(e))}var ot=!1,st=new Map,ct=Symbol();function lt(e){ot=!0;let t=Symbol();ct=t,st.set(t,[]);let n=()=>{for(;st.get(t).length;)st.get(t).shift()();st.delete(t)};e(n),ot=!1,n()}function ut(e){let t=[],n=e=>t.push(e),[r,i]=O(e);return t.push(i),[{Alpine:ur,effect:r,cleanup:n,evaluateLater:F.bind(F,e),evaluate:Be.bind(Be,e)},()=>t.forEach(e=>e())]}function dt(e,t){let n=nt[t.type]||(()=>{}),[r,i]=ut(e);ie(e,t.original,i);let a=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,r),n=n.bind(n,e,t,r),ot?st.get(ct).push(n):n())};return a.runCleanups=i,a}var ft=(e,t)=>({name:n,value:r})=>(n.startsWith(e)&&(n=n.replace(e,t)),{name:n,value:r}),pt=e=>e;function mt(e=()=>{}){return({name:t,value:n})=>{let{name:r,value:i}=ht.reduce((e,t)=>t(e),{name:t,value:n});return r!==t&&e(r,t),{name:r,value:i}}}var ht=[];function gt(e){ht.push(e)}function _t({name:e}){return vt().test(e)}var vt=()=>RegExp(`^${$e}([^:^.]+)\\b`);function yt(e,t){return({name:n,value:r})=>{n===r&&(r=``);let i=n.match(vt()),a=n.match(/:([a-zA-Z0-9\-_:]+)/),o=n.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],s=t||e[n]||n;return{type:i?i[1]:null,value:a?a[1]:null,modifiers:o.map(e=>e.replace(`.`,``)),expression:r,original:s}}}var bt=`DEFAULT`,xt=[`ignore`,`ref`,`id`,`data`,`anchor`,`bind`,`init`,`for`,`model`,`modelable`,`transition`,`show`,`if`,bt,`teleport`];function St(e,t){let n=xt.indexOf(e.type)===-1?bt:e.type,r=xt.indexOf(t.type)===-1?bt:t.type;return xt.indexOf(n)-xt.indexOf(r)}function Ct(e,t){if(typeof ShadowRoot==`function`&&e instanceof ShadowRoot){Array.from(e.children).forEach(e=>Ct(e,t));return}let n=!1;if(t(e,()=>n=!0),n)return;let r=e.firstElementChild;for(;r;)Ct(r,t,!1),r=r.nextElementSibling}var wt=!1;function Tt(e,t=()=>{}){return(...n)=>wt?t(...n):e(...n)}function Et(e){return(...t)=>wt&&e(...t)}var Dt=[];function Ot(e){Dt.push(e)}function kt(e,t){Dt.forEach(n=>n(e,t)),wt=!0,Nt(()=>{nn(t,(e,t)=>{t(e,()=>{})})}),wt=!1}var At=!1;function jt(e,t){t._x_dataStack||=e._x_dataStack,wt=!0,At=!0,Nt(()=>{Mt(t)}),wt=!1,At=!1}function Mt(e){let t=!1;nn(e,(e,n)=>{Ct(e,(e,r)=>{if(t&&Qt(e))return r();t=!0,n(e,r)})})}function Nt(e){let t=b;D((e,n)=>{let r=t(e);return x(r),()=>{}}),e(),D(t)}var Pt=0;function Ft(e,t){let n=e._x_deferInit;n||(n=e._x_deferInit={pending:0,ownsIgnore:!e._x_ignore,queuedAttributes:new Map},n.ownsIgnore&&(e._x_ignore=!0),Pt++),n.pending++,Promise.resolve(t).catch(t=>{try{Pe(t,e)}catch(e){setTimeout(()=>{throw e},0)}}).then(()=>It(e,n))}function It(e,t){t.pending--,!(t.pending>0)&&(pe(),!(t.pending>0)&&e._x_deferInit===t&&(delete e._x_deferInit,t.ownsIgnore&&delete e._x_ignore,Pt--,e.isConnected&&(zt(t),nn(e))))}function Lt(e,t){if(Pt===0)return!1;let n=Zt(e,e=>e._x_deferInit);return n?(Rt(n._x_deferInit,e,t.map(({name:e})=>e)),!0):!1}function Rt(e,t,n){let r=e.queuedAttributes.get(t);(!r||r.marker!==t._x_marker)&&(r={marker:t._x_marker,names:new Set},e.queuedAttributes.set(t,r)),n.forEach(e=>r.names.add(e))}function zt(e){e.queuedAttributes.forEach((e,t)=>{if(!t.isConnected||!t._x_marker||t._x_marker!==e.marker)return;let n=Zt(t,e=>e._x_deferInit);if(n){Rt(n._x_deferInit,t,Array.from(e.names));return}let r=Array.from(e.names).filter(e=>t.hasAttribute(e)).map(e=>({name:e,value:t.getAttribute(e)}));r.length!==0&&it(t,r).forEach(e=>e())})}Ot((e,t)=>{Pt!==0&&e&&e.nodeType===1&&t&&t.nodeType===1&&Zt(e,e=>e._x_deferInit)&&(t._x_ignore=!0)});function Bt(e,t,n={},r={}){return e.dispatchEvent(new CustomEvent(t,{detail:n,bubbles:!0,composed:!0,cancelable:!0,...r}))}function Vt(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var Ht=!1;function Ut(){Ht&&Vt(`Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.`),Ht=!0,document.body||Vt("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `'])); + + $this->assertStringNotContainsString('', $html); + } +} From a5d954f73bc9db03902020ff8b61cad0006bcf10 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:26:06 +0000 Subject: [PATCH 21/36] Prevent framing of MCP authorization and device pages Port laravel/mcp#376 at 79c6fedc6d5dc21cef1040f2e4fbedc8ae980b8d. Register stateless framing middleware through the existing global middleware boundary. Cover all seven Passport authorization/device routes while preserving existing CSP directives and leaving unrelated responses unchanged. Validation: middleware unit tests pass (10/18), real-kernel feature tests pass (2/8), and provider tests pass (2/3). Full source/type analysis and formatting pass. --- src/mcp/src/Server/McpServiceProvider.php | 4 +- .../AddAuthorizationFramingHeaders.php | 31 +++++++++ .../AddAuthorizationFramingHeadersTest.php | 31 +++++++++ .../Unit/Server/McpServiceProviderTest.php | 7 ++ .../AddAuthorizationFramingHeadersTest.php | 68 +++++++++++++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/mcp/src/Server/Middleware/AddAuthorizationFramingHeaders.php create mode 100644 tests/Mcp/Feature/Server/Middleware/AddAuthorizationFramingHeadersTest.php create mode 100644 tests/Mcp/Unit/Server/Middleware/AddAuthorizationFramingHeadersTest.php diff --git a/src/mcp/src/Server/McpServiceProvider.php b/src/mcp/src/Server/McpServiceProvider.php index 0a49286cb5..ce1cf8b0c2 100644 --- a/src/mcp/src/Server/McpServiceProvider.php +++ b/src/mcp/src/Server/McpServiceProvider.php @@ -16,6 +16,7 @@ use Hypervel\Mcp\Console\Commands\MakeToolCommand; use Hypervel\Mcp\Console\Commands\StartCommand; use Hypervel\Mcp\Request; +use Hypervel\Mcp\Server\Middleware\AddAuthorizationFramingHeaders; use Hypervel\Mcp\Server\Middleware\AddWwwAuthenticateHeader; use Hypervel\Support\Facades\Route; use Hypervel\Support\ServiceProvider; @@ -53,13 +54,14 @@ public function boot(): void } /** - * Register the authentication challenge middleware. + * Register the authentication challenge and authorization framing middleware. */ protected function registerGlobalMiddleware(): void { $this->callAfterResolving(HttpKernelContract::class, function (mixed $kernel): void { if ($kernel instanceof HttpKernel) { $kernel->pushMiddleware(AddWwwAuthenticateHeader::class); + $kernel->pushMiddleware(AddAuthorizationFramingHeaders::class); } }); } diff --git a/src/mcp/src/Server/Middleware/AddAuthorizationFramingHeaders.php b/src/mcp/src/Server/Middleware/AddAuthorizationFramingHeaders.php new file mode 100644 index 0000000000..9c1d362ad3 --- /dev/null +++ b/src/mcp/src/Server/Middleware/AddAuthorizationFramingHeaders.php @@ -0,0 +1,31 @@ +routeIs('passport.authorizations.*', 'passport.device', 'passport.device.authorizations.*')) { + return $response; + } + + $response->headers->set('X-Frame-Options', 'DENY'); + $response->headers->set('Content-Security-Policy', "frame-ancestors 'none'", false); + + return $response; + } +} diff --git a/tests/Mcp/Feature/Server/Middleware/AddAuthorizationFramingHeadersTest.php b/tests/Mcp/Feature/Server/Middleware/AddAuthorizationFramingHeadersTest.php new file mode 100644 index 0000000000..6830bfa6b2 --- /dev/null +++ b/tests/Mcp/Feature/Server/Middleware/AddAuthorizationFramingHeadersTest.php @@ -0,0 +1,31 @@ + 'consent')->name('passport.authorizations.authorize'); + + $this->get('oauth/authorize') + ->assertOk() + ->assertHeader('X-Frame-Options', 'DENY') + ->assertHeader('Content-Security-Policy', "frame-ancestors 'none'"); + } + + public function testDoesNotAddFramingHeadersToOtherRoutes(): void + { + Route::get('dashboard', fn (): string => 'ok')->name('dashboard'); + + $this->get('dashboard') + ->assertOk() + ->assertHeaderMissing('X-Frame-Options') + ->assertHeaderMissing('Content-Security-Policy'); + } +} diff --git a/tests/Mcp/Unit/Server/McpServiceProviderTest.php b/tests/Mcp/Unit/Server/McpServiceProviderTest.php index 7963b2b668..6c6cf215ed 100644 --- a/tests/Mcp/Unit/Server/McpServiceProviderTest.php +++ b/tests/Mcp/Unit/Server/McpServiceProviderTest.php @@ -4,7 +4,9 @@ namespace Hypervel\Tests\Mcp\Unit\Server; +use Hypervel\Contracts\Http\Kernel; use Hypervel\Mcp\Server\McpServiceProvider; +use Hypervel\Mcp\Server\Middleware\AddAuthorizationFramingHeaders; use Hypervel\Passport\Passport; use Hypervel\Tests\Mcp\TestCase; @@ -23,4 +25,9 @@ public function testRegistersMcpScopeDuringBoot(): void $this->assertArrayHasKey('mcp:use', Passport::$scopes); $this->assertSame('Custom scope', Passport::$scopes['custom']); } + + public function testRegistersTheAuthorizationFramingMiddlewareGlobally(): void + { + $this->assertTrue(app(Kernel::class)->hasMiddleware(AddAuthorizationFramingHeaders::class)); + } } diff --git a/tests/Mcp/Unit/Server/Middleware/AddAuthorizationFramingHeadersTest.php b/tests/Mcp/Unit/Server/Middleware/AddAuthorizationFramingHeadersTest.php new file mode 100644 index 0000000000..235b15eea7 --- /dev/null +++ b/tests/Mcp/Unit/Server/Middleware/AddAuthorizationFramingHeadersTest.php @@ -0,0 +1,68 @@ +setRouteResolver(fn (): Route => (new Route('GET', '/oauth/authorize', []))->name($name)); + + $response = (new AddAuthorizationFramingHeaders)->handle($request, fn (): Response => response('consent')); + + $this->assertSame('DENY', $response->headers->get('X-Frame-Options')); + $this->assertSame("frame-ancestors 'none'", $response->headers->get('Content-Security-Policy')); + } + + public function testReplacesAWeakerFrameOptionsHeader(): void + { + $request = Request::create('/oauth/authorize'); + $request->setRouteResolver(fn (): Route => (new Route('GET', '/oauth/authorize', []))->name('passport.authorizations.authorize')); + + $response = (new AddAuthorizationFramingHeaders)->handle($request, fn (): Response => response('consent', 200, [ + 'X-Frame-Options' => 'SAMEORIGIN', + ])); + + $this->assertSame(['DENY'], $response->headers->all('x-frame-options')); + } + + public function testKeepsTheContentSecurityPolicyTheApplicationAlreadySet(): void + { + $request = Request::create('/oauth/authorize'); + $request->setRouteResolver(fn (): Route => (new Route('GET', '/oauth/authorize', []))->name('passport.authorizations.authorize')); + + $response = (new AddAuthorizationFramingHeaders)->handle($request, fn (): Response => response('consent', 200, [ + 'Content-Security-Policy' => "default-src 'self'", + ])); + + $this->assertSame(["default-src 'self'", "frame-ancestors 'none'"], $response->headers->all('content-security-policy')); + } + + public function testLeavesOtherResponsesUntouched(): void + { + $request = Request::create('/mcp'); + $request->setRouteResolver(fn (): Route => (new Route('POST', '/mcp', []))->name('mcp')); + + $response = (new AddAuthorizationFramingHeaders)->handle($request, fn (): Response => response('ok')); + + $this->assertFalse($response->headers->has('X-Frame-Options')); + $this->assertFalse($response->headers->has('Content-Security-Policy')); + } +} From 2acc04e1054a73c08c1d80d27120f9e7d9f8ebfd Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:26:06 +0000 Subject: [PATCH 22/36] Preserve batch embedding input order across indexed providers Port laravel/ai#1172 at 1b071b5d9b056a84c33900abe871d51299a0b218. Sort indexed OpenAI and Azure responses before extracting vectors, and apply the same correction to the compatible and OpenRouter gateways. Preserve vector validation and the existing generation/cache flow. Extend existing parsing cases rather than duplicating ordered-response coverage. Validation: shuffled-response regressions fail before the fix. Complete provider files pass: OpenAI 12/17, Azure 11/14, compatible 16/30, OpenRouter 9/16. Full source/type analysis and the AI dogfood suite pass. --- .../AzureOpenAi/AzureOpenAiGateway.php | 2 +- src/ai/src/Gateway/OpenAi/OpenAiGateway.php | 2 +- .../OpenAiCompatibleGateway.php | 2 +- .../Gateway/OpenRouter/OpenRouterGateway.php | 2 +- .../Providers/AzureOpenAi/EmbeddingTest.php | 23 +++++++++++++++++++ .../Providers/OpenAi/EmbeddingTest.php | 21 +++++++++++++++++ .../OpenAiCompatible/EmbeddingsTest.php | 15 ++++++++---- .../Providers/OpenRouter/EmbeddingsTest.php | 15 ++++++++---- 8 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/ai/src/Gateway/AzureOpenAi/AzureOpenAiGateway.php b/src/ai/src/Gateway/AzureOpenAi/AzureOpenAiGateway.php index 5045ff5d50..08de4c9c04 100644 --- a/src/ai/src/Gateway/AzureOpenAi/AzureOpenAiGateway.php +++ b/src/ai/src/Gateway/AzureOpenAi/AzureOpenAiGateway.php @@ -80,7 +80,7 @@ public function generateEmbeddings( $data = $response->json(); return new EmbeddingsResponse( - collect($data['data'] ?? [])->pluck('embedding')->all(), + collect($data['data'] ?? [])->sortBy('index')->pluck('embedding')->all(), new Usage($data['usage']['prompt_tokens'] ?? 0), new Meta($provider->name(), $model), ); diff --git a/src/ai/src/Gateway/OpenAi/OpenAiGateway.php b/src/ai/src/Gateway/OpenAi/OpenAiGateway.php index 0dc1005763..11bafa6a43 100644 --- a/src/ai/src/Gateway/OpenAi/OpenAiGateway.php +++ b/src/ai/src/Gateway/OpenAi/OpenAiGateway.php @@ -287,7 +287,7 @@ public function generateEmbeddings( $data = $response->json(); return new EmbeddingsResponse( - collect($data['data'] ?? [])->pluck('embedding')->all(), + collect($data['data'] ?? [])->sortBy('index')->pluck('embedding')->all(), new Usage($data['usage']['prompt_tokens'] ?? 0), new Meta($provider->name(), $model), ); diff --git a/src/ai/src/Gateway/OpenAiCompatible/OpenAiCompatibleGateway.php b/src/ai/src/Gateway/OpenAiCompatible/OpenAiCompatibleGateway.php index 1b0a1df06e..dfccc65cf5 100644 --- a/src/ai/src/Gateway/OpenAiCompatible/OpenAiCompatibleGateway.php +++ b/src/ai/src/Gateway/OpenAiCompatible/OpenAiCompatibleGateway.php @@ -100,7 +100,7 @@ public function generateEmbeddings( */ protected function parseEmbeddings(array $data, int $expectedCount): array { - $embeddings = (new Collection($data['data'] ?? []))->pluck('embedding'); + $embeddings = (new Collection($data['data'] ?? []))->sortBy('index')->pluck('embedding'); if ($embeddings->count() !== $expectedCount) { throw new AiException('OpenAI-compatible Error: [invalid_response] The response did not contain the expected number of embeddings.'); diff --git a/src/ai/src/Gateway/OpenRouter/OpenRouterGateway.php b/src/ai/src/Gateway/OpenRouter/OpenRouterGateway.php index b9089d1e32..13b34c3490 100644 --- a/src/ai/src/Gateway/OpenRouter/OpenRouterGateway.php +++ b/src/ai/src/Gateway/OpenRouter/OpenRouterGateway.php @@ -349,7 +349,7 @@ public function generateEmbeddings( $this->validateTextResponse($data); return new EmbeddingsResponse( - (new Collection($data['data'] ?? []))->pluck('embedding')->all(), + (new Collection($data['data'] ?? []))->sortBy('index')->pluck('embedding')->all(), new Usage($data['usage']['prompt_tokens'] ?? 0), new Meta($provider->name(), $model), ); diff --git a/tests/Ai/Feature/Providers/AzureOpenAi/EmbeddingTest.php b/tests/Ai/Feature/Providers/AzureOpenAi/EmbeddingTest.php index 1d9cdf67f4..7b68ab0506 100644 --- a/tests/Ai/Feature/Providers/AzureOpenAi/EmbeddingTest.php +++ b/tests/Ai/Feature/Providers/AzureOpenAi/EmbeddingTest.php @@ -103,6 +103,29 @@ public function testMultipleInputsReturnMultipleEmbeddings(): void $this->assertCount(2, $response->embeddings); } + public function testEmbeddingsAreReturnedInInputOrderEvenWhenTheResponseIsShuffled(): void + { + Http::fake(['*' => Http::response([ + 'id' => 'embd-123', + 'object' => 'list', + 'model' => 'text-embedding-3-small', + 'data' => [ + ['object' => 'embedding', 'index' => 2, 'embedding' => [0.7, 0.8, 0.9]], + ['object' => 'embedding', 'index' => 0, 'embedding' => [0.1, 0.2, 0.3]], + ['object' => 'embedding', 'index' => 1, 'embedding' => [0.4, 0.5, 0.6]], + ], + 'usage' => ['prompt_tokens' => 30], + ])]); + + $response = Embeddings::for(['Hello', 'World', 'Again'])->generate(provider: 'azure', model: 'text-embedding-3-small'); + + $this->assertSame([ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + [0.7, 0.8, 0.9], + ], $response->embeddings); + } + public function testEmbeddingsRateLimitResponseThrowsRateLimitedException(): void { $this->expectException(RateLimitedException::class); diff --git a/tests/Ai/Feature/Providers/OpenAi/EmbeddingTest.php b/tests/Ai/Feature/Providers/OpenAi/EmbeddingTest.php index 1261d70d6a..c1c6d2085e 100644 --- a/tests/Ai/Feature/Providers/OpenAi/EmbeddingTest.php +++ b/tests/Ai/Feature/Providers/OpenAi/EmbeddingTest.php @@ -70,6 +70,27 @@ public function testMultipleInputsReturnMultipleEmbeddings(): void $this->assertSame([0.4, 0.5, 0.6], $response->embeddings[1]); } + public function testEmbeddingsAreReturnedInInputOrderEvenWhenTheResponseIsShuffled(): void + { + Http::fake(['*' => Http::response([ + 'object' => 'list', + 'data' => [ + ['object' => 'embedding', 'index' => 2, 'embedding' => [0.7, 0.8, 0.9]], + ['object' => 'embedding', 'index' => 0, 'embedding' => [0.1, 0.2, 0.3]], + ['object' => 'embedding', 'index' => 1, 'embedding' => [0.4, 0.5, 0.6]], + ], + 'usage' => ['prompt_tokens' => 30], + ])]); + + $response = Embeddings::for(['Hello', 'World', 'Again'])->generate(provider: 'openai', model: 'text-embedding-3-small'); + + $this->assertSame([ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + [0.7, 0.8, 0.9], + ], $response->embeddings); + } + public function testEmbeddingsRequestSendsBearerToken(): void { Http::fake(['*' => $this->fakeOpenAiEmbeddingResponse()]); diff --git a/tests/Ai/Feature/Providers/OpenAiCompatible/EmbeddingsTest.php b/tests/Ai/Feature/Providers/OpenAiCompatible/EmbeddingsTest.php index 3dfa68f470..beb2907992 100644 --- a/tests/Ai/Feature/Providers/OpenAiCompatible/EmbeddingsTest.php +++ b/tests/Ai/Feature/Providers/OpenAiCompatible/EmbeddingsTest.php @@ -14,6 +14,7 @@ use Hypervel\Support\Facades\Http; use Hypervel\Tests\Ai\TestCase; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\TestWith; use RuntimeException; class EmbeddingsTest extends TestCase @@ -56,14 +57,18 @@ public function testEmbeddingsRequestIsCorrectlyFormatted(): void }); } - public function testEmbeddingsResponseIsCorrectlyParsed(): void + #[TestWith([false])] + #[TestWith([true])] + public function testEmbeddingsResponseIsCorrectlyParsed(bool $shuffled): void { + $embeddings = [ + ['object' => 'embedding', 'index' => 0, 'embedding' => [0.1, 0.2, 0.3]], + ['object' => 'embedding', 'index' => 1, 'embedding' => [0.4, 0.5, 0.6]], + ]; + Http::fake(['*' => Http::response([ 'object' => 'list', - 'data' => [ - ['object' => 'embedding', 'index' => 0, 'embedding' => [0.1, 0.2, 0.3]], - ['object' => 'embedding', 'index' => 1, 'embedding' => [0.4, 0.5, 0.6]], - ], + 'data' => $shuffled ? array_reverse($embeddings) : $embeddings, 'usage' => ['prompt_tokens' => 10], ])]); diff --git a/tests/Ai/Feature/Providers/OpenRouter/EmbeddingsTest.php b/tests/Ai/Feature/Providers/OpenRouter/EmbeddingsTest.php index a211359765..c67effda97 100644 --- a/tests/Ai/Feature/Providers/OpenRouter/EmbeddingsTest.php +++ b/tests/Ai/Feature/Providers/OpenRouter/EmbeddingsTest.php @@ -12,6 +12,7 @@ use Hypervel\Http\Client\Request; use Hypervel\Support\Facades\Http; use Hypervel\Tests\Ai\TestCase; +use PHPUnit\Framework\Attributes\TestWith; class EmbeddingsTest extends TestCase { @@ -45,14 +46,18 @@ public function testEmbeddingsRequestIsCorrectlyFormatted(): void }); } - public function testEmbeddingsResponseIsCorrectlyParsed(): void + #[TestWith([false])] + #[TestWith([true])] + public function testEmbeddingsResponseIsCorrectlyParsed(bool $shuffled): void { + $embeddings = [ + ['object' => 'embedding', 'index' => 0, 'embedding' => [0.1, 0.2, 0.3]], + ['object' => 'embedding', 'index' => 1, 'embedding' => [0.4, 0.5, 0.6]], + ]; + Http::fake(['*' => Http::response([ 'object' => 'list', - 'data' => [ - ['object' => 'embedding', 'index' => 0, 'embedding' => [0.1, 0.2, 0.3]], - ['object' => 'embedding', 'index' => 1, 'embedding' => [0.4, 0.5, 0.6]], - ], + 'data' => $shuffled ? array_reverse($embeddings) : $embeddings, 'usage' => ['prompt_tokens' => 10], ])]); From c53d45d59844afaf2c78692126d624a42e31fc76 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:29:42 +0000 Subject: [PATCH 23/36] Preserve all Vary values on Inertia and precognitive responses Port inertiajs/inertia-laravel #920 (d25c67c9ec849b743ebbd3d656a0baf8656a93bf) and #919 (4e91cf45cad5ef97862d1ede40fe41616d4e4f91) from the recorded 3.x target 1e8706260b975c1c652dd886a53657e0fae2102d. Normalize Vary into one header line while retaining case-insensitive Inertia detection and final-response placement. Preserve zero, streamed and file responses with the complete upstream assertions. Use native arrays instead of allocating a Collection for each response. Fix the related precognition path dropping later Vary lines by joining every existing value. The multi-line regression fails before the correction. Complete MiddlewareTest (58 tests, 174 assertions) and PrecognitionTest (57 tests, 266 assertions), formatting and source analysis pass. --- .../Middleware/HandlePrecognitiveRequests.php | 3 +- src/inertia/src/Middleware.php | 10 +- tests/Inertia/MiddlewareTest.php | 117 +++++++++++++----- .../Integration/Routing/PrecognitionTest.php | 13 +- 4 files changed, 97 insertions(+), 46 deletions(-) diff --git a/src/foundation/src/Http/Middleware/HandlePrecognitiveRequests.php b/src/foundation/src/Http/Middleware/HandlePrecognitiveRequests.php index cd79f33a57..d30cb58ffb 100644 --- a/src/foundation/src/Http/Middleware/HandlePrecognitiveRequests.php +++ b/src/foundation/src/Http/Middleware/HandlePrecognitiveRequests.php @@ -50,8 +50,9 @@ protected function prepareForPrecognition(Request $request): void */ protected function appendVaryHeader(Request $request, Response $response): Response { + // Content negotiation can add separate Vary lines; retain all of them. return tap($response, fn () => $response->headers->set('Vary', implode(', ', array_filter([ - $response->headers->get('Vary'), + ...$response->headers->all('Vary'), 'Precognition', ])))); } diff --git a/src/inertia/src/Middleware.php b/src/inertia/src/Middleware.php index eb2e48dbba..c17c89c298 100644 --- a/src/inertia/src/Middleware.php +++ b/src/inertia/src/Middleware.php @@ -258,13 +258,11 @@ protected function isPartialReload(Request $request, Response $response): bool */ protected function addInertiaVaryHeader(Response $response): void { - foreach ($response->getVary() as $header) { - if (strcasecmp($header, Header::INERTIA) === 0) { - return; - } - } + // Keep case-insensitive matching without allocating a Collection on every response. + $vary = array_unique(array_filter($response->getVary())); + $present = array_any($vary, static fn (string $header): bool => strcasecmp($header, Header::INERTIA) === 0); - $response->setVary(Header::INERTIA, false); + $response->setVary(implode(', ', $present ? $vary : [...$vary, Header::INERTIA])); } /** diff --git a/tests/Inertia/MiddlewareTest.php b/tests/Inertia/MiddlewareTest.php index d85278e257..0c53ab79bd 100644 --- a/tests/Inertia/MiddlewareTest.php +++ b/tests/Inertia/MiddlewareTest.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Http\Kernel as HttpKernelContract; use Hypervel\Filesystem\Filesystem; +use Hypervel\Foundation\Http\Middleware\HandlePrecognitiveRequests; use Hypervel\Http\JsonResponse; use Hypervel\Http\RedirectResponse; use Hypervel\Http\Request; @@ -31,9 +32,7 @@ use Hypervel\Tests\Inertia\Fixtures\WithoutPreviousLocationMiddleware; use LogicException; use PHPUnit\Framework\Attributes\After; -use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; -use Symfony\Component\HttpFoundation\StreamedResponse; class MiddlewareTest extends TestCase { @@ -99,17 +98,6 @@ public function testNoResponseMeansNoResponseForNonInertiaRequests(): void $this->assertTrue($fooCalled); } - public function testNonInertiaResponsesPreserveExistingVaryHeaders(): void - { - $request = Request::create('/'); - $response = new SymfonyResponse('content', 200, ['Vary' => 'Accept-Encoding']); - - $result = (new Middleware)->handle($request, fn () => $response); - - $this->assertSame($response, $result); - $this->assertSame(['Accept-Encoding', Header::INERTIA], $result->getVary()); - } - public function testInertiaVaryHeaderIsDeduplicatedCaseInsensitively(): void { $request = Request::create('/'); @@ -122,40 +110,44 @@ public function testInertiaVaryHeaderIsDeduplicatedCaseInsensitively(): void $this->assertSame(['x-inertia', 'Accept-Encoding'], $result->getVary()); } - public function testZeroContentIsNotTreatedAsAnEmptyResponse(): void + public function testAResponseBodyOfZeroIsNotTreatedAsEmptyForInertiaRequests(): void { - $request = Request::create('/'); - $request->headers->set(Header::INERTIA, 'true'); - $response = new SymfonyResponse('0'); + $original = response('0'); + Route::middleware(Middleware::class)->get('/', fn () => $original); - $result = (new Middleware)->handle($request, fn () => $response); + $response = $this->from('/foo')->get('/', ['X-Inertia' => 'true']); - $this->assertSame($response, $result); - $this->assertSame('0', $result->getContent()); + $response->assertOk(); + $this->assertSame($original, $response->baseResponse); + $this->assertSame('0', $response->getContent()); } - public function testStreamedResponsesAreNotTreatedAsEmptyResponses(): void + public function testStreamedResponsesAreNotTreatedAsEmptyForInertiaRequests(): void { - $request = Request::create('/'); - $request->headers->set(Header::INERTIA, 'true'); - $response = new StreamedResponse(fn () => print 'content'); + $original = response()->streamDownload(function (): void { + echo 'hello'; + }, 'hello.txt'); + Route::middleware(Middleware::class)->get('/', fn () => $original); - $result = (new Middleware)->handle($request, fn () => $response); + $response = $this->from('/foo')->get('/', ['X-Inertia' => 'true']); - $this->assertSame($response, $result); - $this->assertFalse($result->getContent()); + $response->assertOk(); + $response->assertDownload('hello.txt'); + $this->assertSame($original, $response->baseResponse); + $this->assertFalse($response->baseResponse->getContent()); } - public function testBinaryResponsesAreNotTreatedAsEmptyResponses(): void + public function testFileDownloadsAreNotTreatedAsEmptyForInertiaRequests(): void { - $request = Request::create('/'); - $request->headers->set(Header::INERTIA, 'true'); - $response = new BinaryFileResponse(__FILE__); + $original = response()->download(__DIR__ . '/Fixtures/ExamplePage.vue', 'example.vue'); + Route::middleware(Middleware::class)->get('/', fn () => $original); - $result = (new Middleware)->handle($request, fn () => $response); + $response = $this->from('/foo')->get('/', ['X-Inertia' => 'true']); - $this->assertSame($response, $result); - $this->assertFalse($result->getContent()); + $response->assertOk(); + $response->assertDownload('example.vue'); + $this->assertSame($original, $response->baseResponse); + $this->assertFalse($response->baseResponse->getContent()); } public function testTheVersionIsOptional(): void @@ -908,6 +900,63 @@ public function testDeferredCallbacksAreSkippedOnAFailedResponse(): void $this->assertFalse($called); } + public function testTheVaryHeaderIsSet(): void + { + Route::middleware(Middleware::class)->get('/', function () { + return Inertia::render('User/Edit'); + }); + + $response = $this->get('/'); + + $this->assertSame(['X-Inertia'], $response->headers->all('Vary')); + } + + public function testExistingVaryHeaderValuesArePreserved(): void + { + $original = null; + Route::middleware(Middleware::class)->get('/', function (Request $request) use (&$original) { + return $original = Inertia::render('User/Edit')->toResponse($request)->setVary('Accept-Language'); + }); + + $response = $this->get('/'); + + $this->assertSame($original, $response->baseResponse); + $this->assertSame(['Accept-Language, X-Inertia'], $response->headers->all('Vary')); + } + + public function testTheVaryHeaderIsNotDuplicated(): void + { + Route::middleware(Middleware::class)->get('/', function (Request $request) { + return Inertia::render('User/Edit')->toResponse($request)->setVary('X-Inertia'); + }); + + $response = $this->get('/'); + + $this->assertSame(['X-Inertia'], $response->headers->all('Vary')); + } + + public function testEmptyVaryHeaderValuesAreDiscarded(): void + { + Route::middleware(Middleware::class)->get('/', function (Request $request) { + return Inertia::render('User/Edit')->toResponse($request)->setVary(''); + }); + + $response = $this->get('/'); + + $this->assertSame(['X-Inertia'], $response->headers->all('Vary')); + } + + public function testExistingVaryHeaderValuesArePreservedOnPrecognitiveRoutes(): void + { + Route::middleware([HandlePrecognitiveRequests::class, Middleware::class])->get('/', function (Request $request) { + return Inertia::render('User/Edit')->toResponse($request)->setVary('Accept-Language'); + }); + + $response = $this->get('/'); + + $this->assertSame(['Accept-Language, X-Inertia, Precognition'], $response->headers->all('Vary')); + } + public function testMiddlewareRegistersSsrExceptPaths(): void { $middleware = new SsrExceptMiddleware; diff --git a/tests/Integration/Routing/PrecognitionTest.php b/tests/Integration/Routing/PrecognitionTest.php index 1eef45cb00..f6028ef3e0 100644 --- a/tests/Integration/Routing/PrecognitionTest.php +++ b/tests/Integration/Routing/PrecognitionTest.php @@ -20,6 +20,7 @@ use Hypervel\Support\Facades\Route; use Hypervel\Support\Facades\Validator; use Hypervel\Tests\Integration\Routing\RoutingTestCase; +use PHPUnit\Framework\Attributes\TestWith; function fail() { @@ -637,11 +638,13 @@ public function testItRetainsWildcardIdentityWhenValidatingSpecificInputs(): voi ]); } - public function testItAppendsAnAdditionalVaryHeaderInsteadOfReplacingAnyExistingVaryHeaders(): void + #[TestWith(['Foo', 'Foo, Precognition'])] + #[TestWith([['Foo', 'Prefer'], 'Foo, Prefer, Precognition'])] + public function testItAppendsAnAdditionalVaryHeaderInsteadOfReplacingAnyExistingVaryHeaders(string|array $vary, string $expected): void { - Route::get('test-route', function () { - precognitive(function ($bail) { - $bail(response('expected')->header('Vary', 'Foo')); + Route::get('test-route', function () use ($vary) { + precognitive(function ($bail) use ($vary) { + $bail(response('expected')->header('Vary', $vary)); fail(); }); fail(); @@ -649,7 +652,7 @@ public function testItAppendsAnAdditionalVaryHeaderInsteadOfReplacingAnyExisting $response = $this->get('test-route', ['Precognition' => 'true']); - $response->assertHeader('Vary', 'Foo, Precognition'); + $response->assertHeader('Vary', $expected); $response->assertHeaderMissing('Precognition-Success'); } From 8469e1d0fd8ef30e926cc469c87a44d3762b9002 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:29:42 +0000 Subject: [PATCH 24/36] Invalidate cached Inertia SSR results when the page changes Port inertiajs/inertia-laravel #924 (6431f9a9778ddb3745c75a984947d0f8223b4ab1) from 3.x target 1e8706260b975c1c652dd886a53657e0fae2102d. Apply upstream's page comparison and SSR cache reset inside the existing coroutine-local state. Route response and directive page changes through that boundary, preserving cached results for unchanged pages and request isolation. Port the upstream cross-request test and cover a reproduced changed-page failure within one request. ResponseFactory, Directive, Component and CoroutineIsolation tests pass (104 tests, 228 assertions), along with formatting and source analysis. --- src/inertia/src/Directive.php | 8 +--- src/inertia/src/InertiaState.php | 23 +++++++++-- src/inertia/src/Response.php | 2 +- tests/Inertia/ResponseFactoryTest.php | 57 +++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/inertia/src/Directive.php b/src/inertia/src/Directive.php index ede3142979..7c718acf67 100644 --- a/src/inertia/src/Directive.php +++ b/src/inertia/src/Directive.php @@ -17,9 +17,7 @@ public static function compile(string $expression = ''): string $id = $id === '' ? 'app' : $id; $template = 'page = $page; - $__inertiaSsrResponse = $__inertiaState->dispatchSsr(); + $__inertiaSsrResponse = \Hypervel\Inertia\InertiaState::current()->setPage($page)->dispatchSsr(); if ($__inertiaSsrResponse) { echo $__inertiaSsrResponse->body; @@ -39,9 +37,7 @@ public static function compile(string $expression = ''): string public static function compileHead(string $expression = ''): string { $template = 'page = $page; - $__inertiaSsrResponse = $__inertiaState->dispatchSsr(); + $__inertiaSsrResponse = \Hypervel\Inertia\InertiaState::current()->setPage($page)->dispatchSsr(); if ($__inertiaSsrResponse) { echo $__inertiaSsrResponse->head; diff --git a/src/inertia/src/InertiaState.php b/src/inertia/src/InertiaState.php index bdfbe7d801..25878d50cb 100644 --- a/src/inertia/src/InertiaState.php +++ b/src/inertia/src/InertiaState.php @@ -74,12 +74,12 @@ class InertiaState implements ReplicableContext public array $page = []; /** - * The cached SSR response for the current request. + * The cached SSR response for the current page. */ public ?SsrResponse $ssrResponse = null; /** - * Whether the SSR gateway has been dispatched for this request. + * Whether the SSR gateway has been dispatched for the current page. */ public bool $ssrDispatched = false; @@ -126,7 +126,24 @@ public static function current(): self } /** - * Dispatch SSR if it has not already been dispatched for this request. + * Set the page data for the current request. + * + * @param array $page + */ + public function setPage(array $page): static + { + if ($page !== $this->page) { + $this->ssrDispatched = false; + $this->ssrResponse = null; + } + + $this->page = $page; + + return $this; + } + + /** + * Dispatch SSR if it has not already been dispatched for the current page. */ public function dispatchSsr(): ?SsrResponse { diff --git a/src/inertia/src/Response.php b/src/inertia/src/Response.php index 00a786ff87..a135da92e0 100644 --- a/src/inertia/src/Response.php +++ b/src/inertia/src/Response.php @@ -209,7 +209,7 @@ public function toResponse(Request $request): SymfonyResponse if ($request->header(Header::INERTIA)) { $response = new JsonResponse($page, 200, [Header::INERTIA => 'true']); } else { - InertiaState::current()->page = $page; + InertiaState::current()->setPage($page); $response = ResponseFactory::view($this->rootView, ['page' => $page] + $this->viewData); } diff --git a/tests/Inertia/ResponseFactoryTest.php b/tests/Inertia/ResponseFactoryTest.php index 289cb114e7..99bcf7ab52 100644 --- a/tests/Inertia/ResponseFactoryTest.php +++ b/tests/Inertia/ResponseFactoryTest.php @@ -1142,6 +1142,63 @@ public function testConfigureSsrRequestUsingRegistersTheCallbackWithTheGateway() Http::assertSent(fn (ClientRequest $request): bool => $request->hasHeader('X-Tenant', 'acme')); } + public function testSsrRendersEachPageWhenTheContainerIsReusedBetweenRequests(): void + { + config([ + 'inertia.ssr.enabled' => true, + 'inertia.ssr.ensure_bundle_exists' => false, + ]); + + Http::fake([ + app(HttpGateway::class)->getProductionUrl('/render') => fn (ClientRequest $request) => Http::response(json_encode([ + 'head' => [], + 'body' => '
' . $request['component'] . '
', + ])), + ]); + + Inertia::setRootView('app'); + + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/page-a', function () { + return Inertia::render('Page/A'); + }); + + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/page-b', function () { + return Inertia::render('Page/B'); + }); + + $this->get('/page-a')->assertSee('
Page/A
', false); + $this->get('/page-b')->assertSee('
Page/B
', false); + + Http::assertSentCount(2); + } + + public function testSsrRendersAChangedPageWithinTheSameRequest(): void + { + config([ + 'inertia.ssr.enabled' => true, + 'inertia.ssr.ensure_bundle_exists' => false, + ]); + + Http::fake([ + app(HttpGateway::class)->getProductionUrl('/render') => fn (ClientRequest $request) => Http::response([ + 'head' => [], + 'body' => '
' . $request['component'] . '
', + ]), + ]); + + Inertia::setRootView('app'); + + Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/preview', function () { + Inertia::render('Page/A')->toResponse(request())->getContent(); + + return Inertia::render('Page/B'); + }); + + $this->get('/preview')->assertSee('
Page/B
', false); + + Http::assertSentCount(2); + } + public function testConfigureSsrRequestUsingThrowsWhenTheGatewayDoesNotSupportIt(): void { $this->app->instance(Gateway::class, new FakeGateway); From 1d149829e5080f28c3f941dd581dee29893429d1 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:29:42 +0000 Subject: [PATCH 25/36] Allow SSR startup without stopping an existing server Port inertiajs/inertia-laravel #925 at 3.x target 1e8706260b975c1c652dd886a53657e0fae2102d, including the final --no-stop-before-start option. Document its use in the canonical SSR command guidance; the captured inertia-docs revision f55db8196b7fa8723875e76d1e78c5c3e8077af1 has no corresponding option documentation. Preserve the signal registry and child exit status. Stop the command-owned child in finally when startup, output handling or waiting fails, preventing a live process from surviving its owning coroutine. Cover a real child whose output handler throws, preserving the original exception. Port both upstream option cases through the existing gateway health and shutdown methods. Complete StartSsrTest passes (16 tests, 33 assertions); targeted source analysis and formatting pass. --- src/docs/inertia.md | 6 +++ src/inertia/src/Commands/StartSsr.php | 42 ++++++++++++-------- tests/Inertia/Commands/StartSsrTest.php | 51 +++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/docs/inertia.md b/src/docs/inertia.md index 246d372f58..33a61bcdff 100644 --- a/src/docs/inertia.md +++ b/src/docs/inertia.md @@ -14408,6 +14408,12 @@ The `--runtime` flag on the Artisan command overrides the configured value for a php artisan inertia:start-ssr --runtime=bun ``` +By default, the command stops the server at the configured SSR URL before starting. If that URL points to a shared server or load balancer, use `--no-stop-before-start` to start a new instance without sending a shutdown request. + +```bash +php artisan inertia:start-ssr --no-stop-before-start +``` + You may also enable the `ensure_runtime_exists` option to verify the runtime binary exists before attempting to start the SSR server. The command will exit with an error if the binary cannot be found. ```php diff --git a/src/inertia/src/Commands/StartSsr.php b/src/inertia/src/Commands/StartSsr.php index 95708e89b4..bd5d5c87f1 100644 --- a/src/inertia/src/Commands/StartSsr.php +++ b/src/inertia/src/Commands/StartSsr.php @@ -17,7 +17,9 @@ class StartSsr extends Command /** * The console command name. */ - protected ?string $signature = 'inertia:start-ssr {--runtime= : The runtime to use (e.g. `node`, `bun`, or an absolute path)}'; + protected ?string $signature = 'inertia:start-ssr + {--runtime= : The runtime to use (e.g. `node`, `bun`, or an absolute path)} + {--no-stop-before-start : Do not send a shutdown request to the configured SSR URL before starting}'; /** * The console command description. @@ -60,25 +62,33 @@ public function handle(): int return self::FAILURE; } - $this->callSilently('inertia:stop-ssr'); + if (! $this->option('no-stop-before-start')) { + $this->callSilently('inertia:stop-ssr'); + } $process = app(Process::class, ['command' => [$runtime, $bundle]]); $process->setTimeout(null); - $process->start(); - - $this->trap([SIGINT, SIGQUIT, SIGTERM], function () use ($process): void { - $process->stop(); - }); - - foreach ($process as $type => $data) { - if ($process::OUT === $type) { - $this->info(trim($data)); - } else { - $this->error(trim($data)); - report(new SsrException($data)); + + try { + $process->start(); + + $this->trap([SIGINT, SIGQUIT, SIGTERM], function () use ($process): void { + $process->stop(); + }); + + foreach ($process as $type => $data) { + if ($process::OUT === $type) { + $this->info(trim($data)); + } else { + $this->error(trim($data)); + report(new SsrException($data)); + } } - } - return $process->isSuccessful() ? self::SUCCESS : self::FAILURE; + return $process->isSuccessful() ? self::SUCCESS : self::FAILURE; + } finally { + // The command owns the child; a failed report must not leave it running. + $process->stop(0); + } } } diff --git a/tests/Inertia/Commands/StartSsrTest.php b/tests/Inertia/Commands/StartSsrTest.php index 9deeaf0b02..6613c94931 100644 --- a/tests/Inertia/Commands/StartSsrTest.php +++ b/tests/Inertia/Commands/StartSsrTest.php @@ -4,11 +4,15 @@ namespace Hypervel\Tests\Inertia\Commands; +use Hypervel\Contracts\Debug\ExceptionHandler; use Hypervel\Coroutine\SignalRegistry; use Hypervel\Inertia\Commands\StartSsr; +use Hypervel\Inertia\Ssr\HttpGateway; +use Hypervel\Inertia\Ssr\SsrException; use Hypervel\Tests\Inertia\TestCase; use Mockery as m; use ReflectionProperty; +use RuntimeException; use Symfony\Component\Process\Process; class StartSsrTest extends TestCase @@ -154,6 +158,30 @@ public function testRuntimeIsNotCheckedByDefault(): void $this->assertSame('nonexistent-runtime-binary', $this->processCommand[0]); } + public function testSsrServerIsStoppedBeforeStartingByDefault(): void + { + $this->fakeProcess(); + + $mock = m::mock(HttpGateway::class); + $mock->shouldReceive('checkHealth')->once()->andReturnTrue(); + $mock->shouldReceive('shutdown')->once()->andReturnTrue(); + $this->app->instance(HttpGateway::class, $mock); + + $this->artisan('inertia:start-ssr')->assertExitCode(0); + } + + public function testSsrServerIsNotStoppedWhenTheNoStopBeforeStartOptionIsUsed(): void + { + $this->fakeProcess(); + + $mock = m::mock(HttpGateway::class); + $mock->shouldNotReceive('checkHealth'); + $mock->shouldNotReceive('shutdown'); + $this->app->instance(HttpGateway::class, $mock); + + $this->artisan('inertia:start-ssr', ['--no-stop-before-start' => true])->assertExitCode(0); + } + public function testReturnsFailureWhenTheChildProcessFails(): void { $this->fakeProcess([PHP_BINARY, '-r', 'exit(1);']); @@ -161,6 +189,29 @@ public function testReturnsFailureWhenTheChildProcessFails(): void $this->artisan('inertia:start-ssr')->assertExitCode(1); } + public function testStopsTheChildWhenReportingFails(): void + { + $process = new Process([PHP_BINARY, '-r', 'fwrite(STDERR, "SSR error\n"); sleep(10);']); + $this->app->bind(Process::class, static fn (): Process => $process); + $failure = new RuntimeException('Reporting failed.'); + $this->app->make(ExceptionHandler::class)->reportable(static function (SsrException $exception) use ($failure): never { + throw $failure; + }); + + try { + try { + $this->artisan('inertia:start-ssr', ['--no-stop-before-start' => true])->run(); + $this->fail('The reporting failure was not propagated.'); + } catch (RuntimeException $exception) { + $this->assertSame($failure, $exception); + } + + $this->assertFalse($process->isRunning()); + } finally { + $process->stop(0); + } + } + public function testRegistersTerminationSignalsThroughTheCommandRegistry(): void { $this->fakeProcess(); From cacc5108bf2eeb0b200de581c525aac83138847e Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:30:19 +0000 Subject: [PATCH 26/36] Prevent MCP custom headers from leaking across redirect origins Restrict configured custom headers to the original scheme, host and port in OAuth advertisement probes, message sends and session termination. Keep redirects, destination validation, proxy support and generated MCP session headers intact. Shared middleware changes only each outgoing request, so returning to the original origin retains its configured headers. Extends the OAuth work from laravel/mcp#371 at 79c6fedc6d5dc21cef1040f2e4fbedc8ae980b8d. Document the custom-header boundary and direct destination URL alternative. Verified the affected transport, OAuth endpoint, client and callback test files, full static analysis and formatting. --- src/docs/mcp.md | 15 ++++++ src/docs/porting-from-laravel.md | 2 + src/mcp/README.md | 1 + .../Middleware/RestrictHeadersToOrigin.php | 49 +++++++++++++++++++ .../src/Client/OAuth/AuthServerDiscovery.php | 2 + .../src/Client/Transport/HttpTransport.php | 28 ++++++++++- .../InteractsWithOAuthEndpointsTest.php | 28 +++++++++++ .../Client/Transport/HttpTransportTest.php | 47 ++++++++++++++++++ 8 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 src/mcp/src/Client/Middleware/RestrictHeadersToOrigin.php diff --git a/src/docs/mcp.md b/src/docs/mcp.md index 56dfef67a9..be1466cf24 100644 --- a/src/docs/mcp.md +++ b/src/docs/mcp.md @@ -2062,6 +2062,21 @@ $client = Client::web('https://api.githubcopilot.com/mcp/')->withToken( If you already obtained a suitable token through [Socialite](/docs/{{version}}/socialite) or another account-linking flow, pass that token to `withToken`; you do not need to register MCP OAuth routes. + +#### Custom Headers + +If a server expects an API key or another custom header, use the `withHeaders` method. Named clients may define the same values using the `headers` configuration option: + +```php +use Hypervel\Mcp\Client; + +$client = Client::web('https://projects.example.com/mcp')->withHeaders([ + 'X-Api-Key' => config()->string('services.projects.api_key'), +]); +``` + +Custom headers stay on the configured URL's origin: its scheme, host and port. Redirects to another origin are followed without those headers, so connect to the destination server's URL directly if it needs them. + #### Linking User Accounts With OAuth diff --git a/src/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md index 46f3991ac4..adf060e297 100644 --- a/src/docs/porting-from-laravel.md +++ b/src/docs/porting-from-laravel.md @@ -576,6 +576,8 @@ If an OAuth client relies on Laravel's implicit `mcp:use` scope and the remote s Use `redirect()->signedRoute()` when forwarding `resource_metadata` or `scope` to an MCP client's connect route. Unsigned hints are rejected to prevent credential disclosure through substituted discovery metadata; see [authorization challenges](/docs/{{version}}/mcp#client-oauth-scopes). +If an MCP client's URL redirects to another origin that needs its custom headers, configure the final URL directly. Hypervel follows the redirect without forwarding those headers; see [custom client headers](/docs/{{version}}/mcp#client-custom-headers). + Modern HTTP streams cancel on client disconnect. Review handlers that perform side effects while streaming and ensure their cleanup runs on cancellation; legacy streams still finish. See [streaming responses](/docs/{{version}}/mcp#streaming-responses). MCP servers do not negotiate protocol revision `2025-03-26`. Clients limited to that revision cannot connect; use clients that support `2025-06-18` or newer. See [protocol versions](/docs/{{version}}/mcp#protocol-versions). diff --git a/src/mcp/README.md b/src/mcp/README.md index f637c875e7..132e81af8c 100644 --- a/src/mcp/README.md +++ b/src/mcp/README.md @@ -14,6 +14,7 @@ Documentation: https://hypervel.org/docs/mcp - Local authorization-server discovery belongs to Passport and requires its configured HTTPS issuer. MCP keeps protected-resource discovery and uses `passport.clients.register` for registration. Choose one registration endpoint and define resource audiences in Passport; see [OAuth setup](https://hypervel.org/docs/mcp#oauth). - Anonymous registration persists an explicit scope ceiling from Passport's anonymous allowance plus `mcp:use`; custom client schemas require `scopes` and `third_party`. Wildcard callback domains permit HTTPS and HTTP loopback only; explicit administrator allowances and configured native schemes remain supported. See [registration policy](https://hypervel.org/docs/mcp#oauth). - Connect URLs containing `resource_metadata` or `scope` must be signed with `redirect()->signedRoute()` or `URL::signedRoute()` to prevent discovery-hint tampering; see [authorization challenges](https://hypervel.org/docs/mcp#client-oauth-scopes). +- Custom client headers stay on the configured origin across redirects because they may contain credentials. Connect directly to a server on another origin if it needs those headers; see [custom client headers](https://hypervel.org/docs/mcp#client-custom-headers). - OAuth clients require an explicit policy allowance for private DNS destinations and a DNS substitute for unresolved hostnames in faked tests. See [OAuth destinations](https://hypervel.org/docs/mcp#oauth-destinations) and [testing OAuth clients](https://hypervel.org/docs/mcp#testing-oauth). - OAuth requests use capped response streams instead of Laravel's protected `oAuthCurlOptions()` helper, which is omitted because the transport owns protocol restrictions and rejects its raw cURL options. Customize `$maxOAuthResponseSize` or the request through `oAuthRequest()`. - OAuth clients use the remote server's advertised scopes instead of assuming `mcp:use`. Set `withOAuth(scope: 'mcp:use')` when that scope is required but not advertised; see [OAuth scopes](https://hypervel.org/docs/mcp#client-oauth-scopes). diff --git a/src/mcp/src/Client/Middleware/RestrictHeadersToOrigin.php b/src/mcp/src/Client/Middleware/RestrictHeadersToOrigin.php new file mode 100644 index 0000000000..789219a556 --- /dev/null +++ b/src/mcp/src/Client/Middleware/RestrictHeadersToOrigin.php @@ -0,0 +1,49 @@ + $headers + */ + public function __construct(string $url, protected readonly array $headers) + { + $this->origin = new Uri($url); + } + + /** + * Remove custom headers before a request leaves their origin. + */ + public function __invoke(callable $handler): Closure + { + return function (RequestInterface $request, array $options) use ($handler): PromiseInterface { + if (UriComparator::isCrossOrigin($this->origin, $request->getUri())) { + foreach ($this->headers as $name) { + $request = $request->withoutHeader($name); + } + } + + return $handler($request, $options); + }; + } +} diff --git a/src/mcp/src/Client/OAuth/AuthServerDiscovery.php b/src/mcp/src/Client/OAuth/AuthServerDiscovery.php index 5e90782a87..4156135d81 100644 --- a/src/mcp/src/Client/OAuth/AuthServerDiscovery.php +++ b/src/mcp/src/Client/OAuth/AuthServerDiscovery.php @@ -14,6 +14,7 @@ use Hypervel\Http\Client\RequestException; use Hypervel\Http\Client\Response; use Hypervel\Mcp\Client\Exceptions\OAuthException; +use Hypervel\Mcp\Client\Middleware\RestrictHeadersToOrigin; use Hypervel\Mcp\Client\OAuth\Concerns\InteractsWithOAuthEndpoints; use Hypervel\Support\Arr; use Psr\Http\Message\RequestInterface; @@ -117,6 +118,7 @@ protected function probe(string $resourceUrl, array $headers): Response try { return $this->oAuthRequest() ->withHeaders($headers) + ->withMiddleware(new RestrictHeadersToOrigin($resourceUrl, array_keys($headers))) ->withMiddleware(function (callable $handler): Closure { return function (RequestInterface $request, array $options) use ($handler): PromiseInterface { // Resolve every hop through the same boundary as other OAuth requests, which diff --git a/src/mcp/src/Client/Transport/HttpTransport.php b/src/mcp/src/Client/Transport/HttpTransport.php index c329664025..fc80dfc177 100644 --- a/src/mcp/src/Client/Transport/HttpTransport.php +++ b/src/mcp/src/Client/Transport/HttpTransport.php @@ -8,12 +8,14 @@ use Hypervel\Coroutine\Coroutine; use Hypervel\Http\Client\ConnectionException; use Hypervel\Http\Client\LineReader; +use Hypervel\Http\Client\PendingRequest; use Hypervel\Http\Client\Response as ClientResponse; use Hypervel\Mcp\Client\Contracts\IsolatesExchanges; use Hypervel\Mcp\Client\Contracts\Transport; use Hypervel\Mcp\Client\Contracts\UsesProtocol; use Hypervel\Mcp\Client\Exceptions\AuthorizationRequiredException; use Hypervel\Mcp\Client\Exceptions\TransportException; +use Hypervel\Mcp\Client\Middleware\RestrictHeadersToOrigin; use Hypervel\Mcp\Client\OAuth\WwwAuthenticateChallenge; use Hypervel\Mcp\Enums\ProtocolHandshake; use Hypervel\Mcp\Enums\ProtocolVersion; @@ -193,7 +195,7 @@ public function send(string $message, array $headers = []): void try { try { - $response = Http::withHeaders($this->headers($headers)) + $response = $this->request($headers) ->withBody($message, 'application/json') ->timeout($timeoutSeconds) ->withOptions(['stream' => true]) @@ -383,6 +385,28 @@ protected function headers(array $mirrored = []): array return $headers; } + /** + * Prepare a request with custom headers restricted to the configured origin. + * + * @param array $headers + */ + protected function request(array $headers = []): PendingRequest + { + $request = Http::withHeaders($this->headers($headers)); + + if ($this->customHeaders === []) { + return $request; + } + + // Preserve protocol headers: a redirect target may have issued the session during initialization. + $customHeaders = array_values(array_filter( + array_keys($this->customHeaders), + fn (string $name): bool => ! $this->reserved($name), + )); + + return $request->withMiddleware(new RestrictHeadersToOrigin($this->url, $customHeaders)); + } + /** * Determine whether the header belongs to the MCP protocol. */ @@ -513,7 +537,7 @@ protected function terminateSession(): void } try { - Http::withHeaders($this->headers()) + $this->request() ->timeout($this->timeoutSeconds) ->delete($this->url); } catch (CanceledException $exception) { diff --git a/tests/Mcp/Unit/Client/OAuth/Concerns/InteractsWithOAuthEndpointsTest.php b/tests/Mcp/Unit/Client/OAuth/Concerns/InteractsWithOAuthEndpointsTest.php index 661723e4dc..41652eb6bd 100644 --- a/tests/Mcp/Unit/Client/OAuth/Concerns/InteractsWithOAuthEndpointsTest.php +++ b/tests/Mcp/Unit/Client/OAuth/Concerns/InteractsWithOAuthEndpointsTest.php @@ -147,6 +147,34 @@ public function request(): PendingRequest } } + #[TestWith(['https://other.test/mcp'], 'different host')] + #[TestWith(['https://mcp.test:8443/mcp'], 'different port')] + public function testResourceHeadersStayOnTheirOriginDuringProbeRedirects(string $redirectUrl): void + { + $requests = []; + Http::fake(function ($request) use (&$requests): void { + $requests[] = $request; + }); + Http::fake([ + 'https://mcp.test/mcp' => Http::response('', 307, ['Location' => 'https://mcp.test:443/next']), + 'https://mcp.test/next' => Http::response('', 307, ['Location' => $redirectUrl]), + $redirectUrl => Http::response('', 401, [ + 'WWW-Authenticate' => 'Bearer resource_metadata="https://metadata.test/resource"', + ]), + ]); + + (new AuthServerDiscovery)->requireAdvertised( + 'https://mcp.test/mcp', + 'https://metadata.test/resource', + ['X-Api-Key' => 'resource-secret'], + ); + + $this->assertCount(3, $requests); + $this->assertTrue($requests[0]->hasHeader('X-Api-Key', 'resource-secret')); + $this->assertTrue($requests[1]->hasHeader('X-Api-Key', 'resource-secret')); + $this->assertFalse($requests[2]->hasHeader('X-Api-Key')); + } + #[TestWith([false], 'environment proxy')] #[TestWith([true], 'policy proxy')] public function testProbesPreserveHttpsProxiesAndPinsOnEachHop(bool $policyProxy): void diff --git a/tests/Mcp/Unit/Client/Transport/HttpTransportTest.php b/tests/Mcp/Unit/Client/Transport/HttpTransportTest.php index 3c3a5ccc9e..2eda1c2315 100644 --- a/tests/Mcp/Unit/Client/Transport/HttpTransportTest.php +++ b/tests/Mcp/Unit/Client/Transport/HttpTransportTest.php @@ -260,6 +260,53 @@ public function testReplacesADefaultHeaderCaseInsensitivelyInsteadOfSendingADupl }); } + #[TestWith([false], 'message')] + #[TestWith([true], 'session termination')] + public function testRedirectsKeepCustomHeadersOnTheirOrigin(bool $disconnect): void + { + $requests = []; + Http::fake(function (Request $request) use (&$requests): PromiseInterface { + if (($request['method'] ?? null) === 'initialize') { + return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => []], headers: [ + 'MCP-Session-Id' => 'resource-session', + ]); + } + + $requests[] = $request; + + return match ($request->url()) { + 'https://mcp.test/mcp' => Http::response('', 307, ['Location' => 'https://other.test/mcp']), + 'https://other.test/mcp' => Http::response('', 307, ['Location' => 'https://mcp.test/return']), + default => Http::response('', 202), + }; + }); + $transport = new HttpTransport('https://mcp.test/mcp'); + $transport->withHeaders(['X-Api-Key' => 'resource-secret', 'MCP-Protocol-Version' => 'ignored']); + $transport->useProtocol(ProtocolVersion::V2025_11_25); + + try { + $transport->send(json_encode(['jsonrpc' => '2.0', 'id' => 1, 'method' => 'initialize'])); + $transport->receive(); + + if ($disconnect) { + $transport->disconnect(); + } else { + $transport->send(json_encode(['jsonrpc' => '2.0', 'id' => 2, 'method' => 'ping'])); + } + + $this->assertCount(3, $requests); + foreach ([0, 2] as $index) { + $this->assertTrue($requests[$index]->hasHeader('X-Api-Key', 'resource-secret')); + $this->assertTrue($requests[$index]->hasHeader('MCP-Session-Id', 'resource-session')); + } + $this->assertFalse($requests[1]->hasHeader('X-Api-Key')); + $this->assertTrue($requests[1]->hasHeader('MCP-Session-Id', 'resource-session')); + $this->assertTrue($requests[1]->hasHeader('MCP-Protocol-Version', ProtocolVersion::V2025_11_25->value)); + } finally { + $transport->disconnect(); + } + } + public function testLetsACustomAuthorizationHeaderOverrideTheBearerToken(): void { Http::fake(['*' => Http::response(json_encode(['jsonrpc' => '2.0', 'id' => 1, 'result' => []]), 200, ['Content-Type' => 'application/json'])]); From e89b060d0808d907f6547119e25a0477f7eeb61b Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:30:19 +0000 Subject: [PATCH 27/36] Preserve AnyOf field values and reject nested exists candidates Distinguish field maps by string keys so filtered numeric rule lists remain lists, and keep array-valued fields intact when evaluating keyed alternatives. Reject nested exists candidates before array_unique and database binding instead of raising warnings while trying an alternative. Corrects behavior encountered while porting laravel/framework#62171 from 13.x at 5d7797528723107cd4f495326838a7d7d67fbb20. Batch database validation already excludes these candidates; no additional batching guard is needed. Verified complete AnyOf and Validator test files, full static analysis, targeted analysis after the callback rename, and formatting. --- .../src/Concerns/ValidatesAttributes.php | 9 ++++++ src/validation/src/Rules/AnyOf.php | 5 +-- tests/Validation/ValidationAnyOfRuleTest.php | 32 +++++++++++++++++++ tests/Validation/ValidationValidatorTest.php | 3 ++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/validation/src/Concerns/ValidatesAttributes.php b/src/validation/src/Concerns/ValidatesAttributes.php index 817bce46e5..7272406e82 100644 --- a/src/validation/src/Concerns/ValidatesAttributes.php +++ b/src/validation/src/Concerns/ValidatesAttributes.php @@ -1082,6 +1082,15 @@ public function validateExists(string $attribute, mixed $value, mixed $parameter // that the columns being "verified" shares the given attribute's name. $column = $this->getQueryColumn($parameters, $attribute); + // Nested arrays cannot be compared by array_unique() or bound by whereIn(). + if (is_array($value)) { + foreach ($value as $element) { + if (is_array($element)) { + return false; + } + } + } + $expected = is_array($value) ? count(array_unique($value)) : 1; if ($expected === 0) { diff --git a/src/validation/src/Rules/AnyOf.php b/src/validation/src/Rules/AnyOf.php index b7a613e603..87c16aadb0 100644 --- a/src/validation/src/Rules/AnyOf.php +++ b/src/validation/src/Rules/AnyOf.php @@ -36,10 +36,11 @@ public function __construct(array $rules) public function passes(string $attribute, mixed $value): bool { foreach ($this->rules as $rule) { - $keyed = Arr::isAssoc(Arr::wrap($rule)); + // Filtered rule lists keep numeric gaps, so only string keys identify a field map. + $keyed = array_any(Arr::wrap($rule), static fn (mixed $definition, int|string $key): bool => is_string($key)); $validator = Validator::make( - $keyed && Arr::isAssoc(Arr::wrap($value)) ? $value : [$value], + $keyed ? Arr::wrap($value) : [$value], $keyed ? $rule : [$rule], $this->validator->customMessages, // @phpstan-ignore property.notFound $this->validator->customAttributes // @phpstan-ignore property.notFound diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 5fe9722097..68b4db9569 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -10,6 +10,7 @@ use Hypervel\Translation\Translator; use Hypervel\Validation\Rule; use Hypervel\Validation\Validator; +use PHPUnit\Framework\Attributes\DataProvider; enum TaggedUnionDiscriminatorType: string { @@ -156,6 +157,37 @@ public function testAssociativeArrayValueAgainstPlainRules(): void $this->assertTrue($validator->passes()); } + #[DataProvider('alternativeRuleShapes')] + public function testAlternativeRulesPreserveTheirDataShape(array $rules, mixed $value, bool $passes): void + { + $validator = new Validator($this->app->make('translator'), ['data' => $value], [ + 'data' => Rule::anyOf([$rules]), + ]); + + $this->assertSame($passes, $validator->passes()); + } + + /** + * Provide filtered rule lists and keyed alternatives. + */ + public static function alternativeRuleShapes(): iterable + { + yield 'filtered list rejects invalid email' => [array_filter(['string', null, 'email']), 'not-an-email', false]; + yield 'filtered list accepts valid email' => [array_filter(['string', null, 'email']), 'test@example.com', true]; + yield 'wildcard rejects invalid list item' => [['*.email' => 'email'], [['email' => 'invalid']], false]; + yield 'wildcard accepts valid list item' => [['*.email' => 'email'], [['email' => 'test@example.com']], true]; + yield 'keyed rules reject a scalar missing required fields' => [['email' => 'required|email'], 'invalid', false]; + } + + public function testNestedArraysCanUseAnArrayAlternativeAfterExists(): void + { + $validator = new Validator($this->app->make('translator'), ['data' => ['a' => [1], 'b' => [2]]], [ + 'data' => Rule::anyOf(['exists:users,id', 'array']), + ]); + + $this->assertTrue($validator->passes()); + } + public function testTaggedUnionObjects(): void { $validator = new Validator($this->app->make('translator'), [ diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 47a7a77abd..36f7b7307f 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -5025,6 +5025,9 @@ public function testValidationExists(): void $mock->expects('getMultiCount')->with('users', 'email_addr', ['foo', 'foo'], [])->andReturn(1); $v->setPresenceVerifier($mock); $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['email' => ['a' => ['foo']]], ['email' => 'exists:users,email_addr']); + $this->assertFalse($v->passes()); } public function testValidationExistsIsNotCalledUnnecessarily(): void From e6836a12d65924b994a7d23b64e6db590d53b7ce Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:30:19 +0000 Subject: [PATCH 28/36] Validate and order indexed embedding responses in one shared parser Reject mismatched counts and duplicate, out-of-range or non-integer indices before mapping vectors to their inputs. Place records directly into input order in linear time, sharing the parser across OpenAI, Azure OpenAI, OpenRouter and compatible gateways. Preserve response-order fallback for omitted indices, base64 values and compatible-provider vector validation. Extends laravel/ai#1172 at 1b071b5d9b056a84c33900abe871d51299a0b218. Native array_map avoids a Collection allocation in the compatible parser. Verified all four gateway embedding test files, shared invalid-index cases, full static analysis, targeted analysis after the native mapping change, and formatting. --- .../AzureOpenAi/AzureOpenAiGateway.php | 4 +- .../Concerns/ParsesIndexedEmbeddings.php | 40 +++++++++++++++++++ src/ai/src/Gateway/OpenAi/OpenAiGateway.php | 4 +- .../OpenAiCompatibleGateway.php | 13 ++---- .../Gateway/OpenRouter/OpenRouterGateway.php | 4 +- .../OpenAiCompatible/EmbeddingsTest.php | 30 +++++++++++++- 6 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 src/ai/src/Gateway/Concerns/ParsesIndexedEmbeddings.php diff --git a/src/ai/src/Gateway/AzureOpenAi/AzureOpenAiGateway.php b/src/ai/src/Gateway/AzureOpenAi/AzureOpenAiGateway.php index 08de4c9c04..57c025d9eb 100644 --- a/src/ai/src/Gateway/AzureOpenAi/AzureOpenAiGateway.php +++ b/src/ai/src/Gateway/AzureOpenAi/AzureOpenAiGateway.php @@ -14,6 +14,7 @@ use Hypervel\Ai\Files\Image; use Hypervel\Ai\Gateway\AzureOpenAi\Concerns\CreatesAzureOpenAiClient; use Hypervel\Ai\Gateway\Concerns\HandlesFailoverErrors; +use Hypervel\Ai\Gateway\Concerns\ParsesIndexedEmbeddings; use Hypervel\Ai\Gateway\Concerns\ParsesServerSentEvents; use Hypervel\Ai\Gateway\OpenAi\Concerns\BuildsTextRequests; use Hypervel\Ai\Gateway\OpenAi\Concerns\HandlesTextGeneration; @@ -46,6 +47,7 @@ class AzureOpenAiGateway implements EmbeddingGateway, ImageGateway, StepTextGate use MapsAttachments; use MapsMessages; use MapsTools; + use ParsesIndexedEmbeddings; use ParsesServerSentEvents; use ParsesTextResponses; @@ -80,7 +82,7 @@ public function generateEmbeddings( $data = $response->json(); return new EmbeddingsResponse( - collect($data['data'] ?? [])->sortBy('index')->pluck('embedding')->all(), + $this->parseIndexedEmbeddings($data['data'] ?? [], count($inputs), $provider->name()), new Usage($data['usage']['prompt_tokens'] ?? 0), new Meta($provider->name(), $model), ); diff --git a/src/ai/src/Gateway/Concerns/ParsesIndexedEmbeddings.php b/src/ai/src/Gateway/Concerns/ParsesIndexedEmbeddings.php new file mode 100644 index 0000000000..b4ffb9f2e4 --- /dev/null +++ b/src/ai/src/Gateway/Concerns/ParsesIndexedEmbeddings.php @@ -0,0 +1,40 @@ + $records + * @return list + */ + protected function parseIndexedEmbeddings(array $records, int $expectedCount, string $providerName): array + { + if (count($records) !== $expectedCount) { + throw new AiException("{$providerName} Error: [invalid_response] The response did not contain the expected number of embeddings."); + } + + $ordered = array_fill(0, $expectedCount, null); + + foreach ($records as $position => $record) { + // Some compatible servers omit indices; retain their response order. + $index = is_array($record) ? ($record['index'] ?? $position) : null; + + if (! is_int($index) || $index < 0 || $index >= $expectedCount || $ordered[$index] !== null) { + throw new AiException("{$providerName} Error: [invalid_response] The response contained invalid embedding indices."); + } + + $ordered[$index] = $record; + } + + // Matching counts and unique in-range indices ensure every slot contains a record. + /** @var list> $ordered */ + return array_map(static fn (array $record): mixed => $record['embedding'] ?? null, $ordered); + } +} diff --git a/src/ai/src/Gateway/OpenAi/OpenAiGateway.php b/src/ai/src/Gateway/OpenAi/OpenAiGateway.php index 11bafa6a43..ffb64b4e56 100644 --- a/src/ai/src/Gateway/OpenAi/OpenAiGateway.php +++ b/src/ai/src/Gateway/OpenAi/OpenAiGateway.php @@ -14,6 +14,7 @@ use Hypervel\Ai\Contracts\Providers\TranscriptionProvider; use Hypervel\Ai\Files\Image; use Hypervel\Ai\Gateway\Concerns\HandlesFailoverErrors; +use Hypervel\Ai\Gateway\Concerns\ParsesIndexedEmbeddings; use Hypervel\Ai\Gateway\Concerns\ParsesServerSentEvents; use Hypervel\Ai\Gateway\Concerns\ResolvesAudioFilenames; use Hypervel\Ai\Gateway\OpenAi\Concerns\BuildsTextRequests; @@ -52,6 +53,7 @@ class OpenAiGateway implements Gateway, StepTextGateway use MapsTools; use ParsesTextResponses; use HandlesFailoverErrors; + use ParsesIndexedEmbeddings; use ParsesServerSentEvents; use ResolvesAudioFilenames; @@ -287,7 +289,7 @@ public function generateEmbeddings( $data = $response->json(); return new EmbeddingsResponse( - collect($data['data'] ?? [])->sortBy('index')->pluck('embedding')->all(), + $this->parseIndexedEmbeddings($data['data'] ?? [], count($inputs), $provider->name()), new Usage($data['usage']['prompt_tokens'] ?? 0), new Meta($provider->name(), $model), ); diff --git a/src/ai/src/Gateway/OpenAiCompatible/OpenAiCompatibleGateway.php b/src/ai/src/Gateway/OpenAiCompatible/OpenAiCompatibleGateway.php index dfccc65cf5..1802f28f1a 100644 --- a/src/ai/src/Gateway/OpenAiCompatible/OpenAiCompatibleGateway.php +++ b/src/ai/src/Gateway/OpenAiCompatible/OpenAiCompatibleGateway.php @@ -12,6 +12,7 @@ use Hypervel\Ai\Contracts\Providers\TranscriptionProvider; use Hypervel\Ai\Exceptions\AiException; use Hypervel\Ai\Gateway\Concerns\HandlesFailoverErrors; +use Hypervel\Ai\Gateway\Concerns\ParsesIndexedEmbeddings; use Hypervel\Ai\Gateway\Concerns\ParsesServerSentEvents; use Hypervel\Ai\Gateway\Concerns\ResolvesAudioFilenames; use Hypervel\Ai\Gateway\OpenAiCompatible\Concerns\BuildsTextRequests; @@ -31,7 +32,6 @@ use Hypervel\Ai\Responses\TranscriptionResponse; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Support\Arr; -use Hypervel\Support\Collection; use InvalidArgumentException; class OpenAiCompatibleGateway implements EmbeddingGateway, StepTextGateway, TranscriptionGateway @@ -45,6 +45,7 @@ class OpenAiCompatibleGateway implements EmbeddingGateway, StepTextGateway, Tran use ParsesTextResponses; use PerformsChatCompletionSteps; use HandlesFailoverErrors; + use ParsesIndexedEmbeddings; use ParsesServerSentEvents; use ResolvesAudioFilenames; @@ -100,19 +101,13 @@ public function generateEmbeddings( */ protected function parseEmbeddings(array $data, int $expectedCount): array { - $embeddings = (new Collection($data['data'] ?? []))->sortBy('index')->pluck('embedding'); - - if ($embeddings->count() !== $expectedCount) { - throw new AiException('OpenAI-compatible Error: [invalid_response] The response did not contain the expected number of embeddings.'); - } - - return $embeddings->map(function (mixed $embedding): array { + return array_map(static function (mixed $embedding): array { if (! is_array($embedding) || $embedding === [] || array_filter($embedding, 'is_numeric') !== $embedding) { throw new AiException('OpenAI-compatible Error: [invalid_response] The response contained an invalid embedding vector.'); } return array_map(floatval(...), $embedding); - })->all(); + }, $this->parseIndexedEmbeddings($data['data'] ?? [], $expectedCount, 'OpenAI-compatible')); } /** diff --git a/src/ai/src/Gateway/OpenRouter/OpenRouterGateway.php b/src/ai/src/Gateway/OpenRouter/OpenRouterGateway.php index 13b34c3490..59037ec89e 100644 --- a/src/ai/src/Gateway/OpenRouter/OpenRouterGateway.php +++ b/src/ai/src/Gateway/OpenRouter/OpenRouterGateway.php @@ -16,6 +16,7 @@ use Hypervel\Ai\Contracts\Providers\SupportsWebSearch; use Hypervel\Ai\Contracts\Providers\TranscriptionProvider; use Hypervel\Ai\Gateway\Concerns\HandlesFailoverErrors; +use Hypervel\Ai\Gateway\Concerns\ParsesIndexedEmbeddings; use Hypervel\Ai\Gateway\Concerns\ParsesServerSentEvents; use Hypervel\Ai\Gateway\Concerns\WrapsPcmAudio; use Hypervel\Ai\Gateway\OpenAiCompatible\Concerns\MapsChatCompletionTools; @@ -59,6 +60,7 @@ class OpenRouterGateway implements Gateway, RerankingGateway, StepTextGateway use ParsesTextResponses; use HandlesFailoverErrors; use MapsChatCompletionTools; + use ParsesIndexedEmbeddings; use ParsesServerSentEvents; use PerformsChatCompletionSteps; use WrapsPcmAudio; @@ -349,7 +351,7 @@ public function generateEmbeddings( $this->validateTextResponse($data); return new EmbeddingsResponse( - (new Collection($data['data'] ?? []))->sortBy('index')->pluck('embedding')->all(), + $this->parseIndexedEmbeddings($data['data'] ?? [], count($inputs), $provider->name()), new Usage($data['usage']['prompt_tokens'] ?? 0), new Meta($provider->name(), $model), ); diff --git a/tests/Ai/Feature/Providers/OpenAiCompatible/EmbeddingsTest.php b/tests/Ai/Feature/Providers/OpenAiCompatible/EmbeddingsTest.php index beb2907992..ee15e55155 100644 --- a/tests/Ai/Feature/Providers/OpenAiCompatible/EmbeddingsTest.php +++ b/tests/Ai/Feature/Providers/OpenAiCompatible/EmbeddingsTest.php @@ -59,13 +59,18 @@ public function testEmbeddingsRequestIsCorrectlyFormatted(): void #[TestWith([false])] #[TestWith([true])] - public function testEmbeddingsResponseIsCorrectlyParsed(bool $shuffled): void + #[TestWith([false, true], 'response order without indices')] + public function testEmbeddingsResponseIsCorrectlyParsed(bool $shuffled, bool $omitIndices = false): void { $embeddings = [ ['object' => 'embedding', 'index' => 0, 'embedding' => [0.1, 0.2, 0.3]], ['object' => 'embedding', 'index' => 1, 'embedding' => [0.4, 0.5, 0.6]], ]; + if ($omitIndices) { + unset($embeddings[0]['index'], $embeddings[1]['index']); + } + Http::fake(['*' => Http::response([ 'object' => 'list', 'data' => $shuffled ? array_reverse($embeddings) : $embeddings, @@ -242,6 +247,29 @@ public function testEmbeddingsRejectMalformedVectors(): void Ai::instance('openai-compatible')->embeddings(['Hello']); } + #[TestWith([['index' => 0, 'embedding' => [0.3]]], 'duplicate')] + #[TestWith([['index' => 3, 'embedding' => [0.3]]], 'out of range')] + #[TestWith([['index' => -1, 'embedding' => [0.3]]], 'negative')] + #[TestWith([['index' => '2', 'embedding' => [0.3]]], 'non-integer')] + #[TestWith(['not-a-record'], 'non-array record')] + public function testEmbeddingsRejectInvalidIndices(mixed $invalidRecord): void + { + Http::fake(['*' => Http::response([ + 'object' => 'list', + 'data' => [ + ['index' => 0, 'embedding' => [0.1]], + ['index' => 1, 'embedding' => [0.2]], + $invalidRecord, + ], + 'usage' => ['prompt_tokens' => 3], + ])]); + + $this->expectException(AiException::class); + $this->expectExceptionMessage('embedding indices'); + + Ai::instance('openai-compatible')->embeddings(['First', 'Second', 'Third']); + } + public function testEmbeddingsRateLimitResponseThrowsRateLimitedException(): void { $this->expectException(RateLimitedException::class); From 195277f8bd0fe407e88b06bfa8e0371ec1e70652 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:30:19 +0000 Subject: [PATCH 29/36] Reuse existing pivot links when createOrFirst recovers a model After a unique create conflict, check whether the recovered model is already attached before inserting another pivot row. This prevents duplicate links on pivot tables without a unique constraint and also covers restoreOrCreate recovery. Successful creates keep their existing queries; existing but unattached models require one additional existence lookup. Retain the post-attach unique-conflict check for concurrent attachment and leave firstOrCreate and updateOrCreate unchanged. Compared against laravel/framework 13.x at 5d7797528723107cd4f495326838a7d7d67fbb20 while reconciling #62146. Verified createOrFirst query expectations, relation restore regressions and the existing BelongsToMany SQLite integration file, full static analysis and formatting. --- src/database/src/Eloquent/Relations/BelongsToMany.php | 5 +++++ ...DatabaseEloquentBelongsToManyCreateOrFirstTest.php | 11 +++++++++-- .../Database/EloquentRelationRestoreOrCreateTest.php | 5 +++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/database/src/Eloquent/Relations/BelongsToMany.php b/src/database/src/Eloquent/Relations/BelongsToMany.php index 1efa5df918..92f4268f75 100644 --- a/src/database/src/Eloquent/Relations/BelongsToMany.php +++ b/src/database/src/Eloquent/Relations/BelongsToMany.php @@ -698,6 +698,11 @@ public function createOrFirst(array $attributes = [], Closure|array $values = [] $instance = $this->related->newQuery()->withoutGlobalScopes($this->query->removedScopes()) ->where($attributes)->useWritePdo()->first() ?? throw $exception; + // The recovered model may already be linked even without a unique constraint on the pivot table. + if ($this->hasAttachedPivot($instance)) { + return $instance; + } + try { $this->getQuery()->withSavepointIfNeeded(fn () => $this->attach($instance, $joining, $touch)); } catch (UniqueConstraintViolationException $attachException) { diff --git a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php index 4826dede60..c447baebc6 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php @@ -101,6 +101,12 @@ public function testCreateOrFirstMethodAssociatesExistingRelated(): void 'updated_at' => '2023-01-01 00:00:00', ]]); + $source->getConnection()->expects('select')->with( + 'select exists(select * from "pivot_table" where "pivot_table"."source_id" = ? and "pivot_table"."related_id" in (?)) as "exists"', + [123, 456], + false, + )->andReturn([['exists' => 0]]); + $source->getConnection()->expects('insert')->with( 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)', [456, 123], @@ -204,12 +210,13 @@ public function testCreateOrFirstMethodRetrievesExistingRelatedAssociatedJustNow $source->getConnection() ->expects('select') + ->twice() ->with( 'select exists(select * from "pivot_table" where "pivot_table"."source_id" = ? and "pivot_table"."related_id" in (?)) as "exists"', [123, 456], false, ) - ->andReturn([['exists' => 1]]); + ->andReturn([['exists' => 0]], [['exists' => 1]]); $result = $source->related()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); $this->assertFalse($result->wasRecentlyCreated); @@ -259,7 +266,7 @@ public function testCreateOrFirstMethodRethrowsAttachViolationWhenExactPivotIsMi 'select exists(select * from "pivot_table" where "pivot_table"."source_id" = ? and "pivot_table"."related_id" in (?)) as "exists"', [123, 456], false, - )->andReturn([['exists' => 0]]); + )->twice()->andReturn([['exists' => 0]]); try { $source->related()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); diff --git a/tests/Integration/Database/EloquentRelationRestoreOrCreateTest.php b/tests/Integration/Database/EloquentRelationRestoreOrCreateTest.php index bfcb16a425..80c78fcccd 100644 --- a/tests/Integration/Database/EloquentRelationRestoreOrCreateTest.php +++ b/tests/Integration/Database/EloquentRelationRestoreOrCreateTest.php @@ -133,6 +133,11 @@ public function testCreateOrRestoreOnBelongsToMany(): void $this->assertTrue($restored->is($created)); $this->assertFalse($restored->trashed()); $this->assertTrue($user->roles()->whereKey($created->getKey())->exists()); + + $restored->delete(); + $user->roles()->createOrRestore(['id' => $created->id], ['name' => 'admin']); + + $this->assertSame(1, $user->roles()->count()); } } From 4b5771f2b4cfe7ce26df06e61155efe9ccec25d7 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:30:19 +0000 Subject: [PATCH 30/36] Retain retry options when object payloads omit commandName Fall back to the required handler string when optional commandName metadata is absent. Keep SQS FIFO defaults available for object jobs that cannot be restored, without changing string-job payload classification. Corrects recovery introduced with laravel/framework#62124, compared at 13.x revision 5d7797528723107cd4f495326838a7d7d67fbb20. Verified the complete QueueRetryCommand test file, full static analysis and formatting. --- src/queue/src/Console/RetryCommand.php | 2 +- tests/Queue/QueueRetryCommandTest.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/queue/src/Console/RetryCommand.php b/src/queue/src/Console/RetryCommand.php index bc44564bda..ef5ec088df 100644 --- a/src/queue/src/Console/RetryCommand.php +++ b/src/queue/src/Console/RetryCommand.php @@ -234,7 +234,7 @@ protected function getQueueableOptions(Queue $queue, stdClass $job): array // Unrestorable jobs still need the driver's string-job defaults, including required FIFO message options. if ($instance === null || $instance instanceof __PHP_Incomplete_Class) { - $instance = $payload['data']['commandName']; + $instance = $payload['data']['commandName'] ?? $payload['job']; } $getOptions = fn (SqsQueue $queue) => $queue->getQueueableOptions( diff --git a/tests/Queue/QueueRetryCommandTest.php b/tests/Queue/QueueRetryCommandTest.php index 7e01a5ca5e..6775e26ba4 100644 --- a/tests/Queue/QueueRetryCommandTest.php +++ b/tests/Queue/QueueRetryCommandTest.php @@ -655,6 +655,10 @@ public static function jobsWithoutRestorableObjects(): iterable yield 'removed class on FIFO' => ['default.fifo', $incomplete]; yield 'removed class on standard' => ['default', $incomplete]; + yield 'removed class without optional command name' => ['default.fifo', [ + 'job' => 'CustomObjectHandler@call', + 'data' => ['command' => 'O:10:"RemovedJob":0:{}'], + ]]; } public function testDispatchesTheEventBeforePushingAndForgetsTheJobAfterwards(): void From 3c49a5c314099212c1f9fe7999499dfe2baeb768 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:30:19 +0000 Subject: [PATCH 31/36] Clear current route before listeners and direct route dispatch Move coroutine-local current-route clearing into findRoute before Routing listeners run. Both dispatch and direct dispatchToRoute now clear stale route state, including failed route matching, without duplicate writes on ordinary dispatch. Completes laravel/framework#62123 behavior compared at 13.x revision 5d7797528723107cd4f495326838a7d7d67fbb20 while retaining coroutine isolation. Verified complete RoutingRouteTest and RouterExtensionTest files, full static analysis and formatting. --- src/routing/src/Router.php | 5 +++-- tests/Routing/RoutingRouteTest.php | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/routing/src/Router.php b/src/routing/src/Router.php index 4f2c681d5f..083b2c774d 100644 --- a/src/routing/src/Router.php +++ b/src/routing/src/Router.php @@ -609,8 +609,6 @@ public function respondWithRoute(string $name): SymfonyResponse public function dispatch(Request $request): SymfonyResponse { CoroutineContext::set(self::CURRENT_REQUEST_CONTEXT_KEY, $request); - // A failed match must not retain an earlier dispatch's route in this coroutine. - CoroutineContext::set(self::CURRENT_ROUTE_CONTEXT_KEY, null); return $this->dispatchToRoute($request); } @@ -659,6 +657,9 @@ public function dispatchToCallback(Request $request, Closure $callback): Symfony */ protected function findRoute(Request $request): Route { + // A failed match must not retain an earlier dispatch's route in this coroutine. + CoroutineContext::set(self::CURRENT_ROUTE_CONTEXT_KEY, null); + if ($this->events->hasListeners(Routing::class)) { $this->events->dispatch(new Routing($request)); } diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index 844ec681b7..3b1e0e087f 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -45,6 +45,7 @@ use Hypervel\Tests\Routing\Fixtures\CategoryBackedEnum; use Laravel\SerializableClosure\SerializableClosure; use LogicException; +use PHPUnit\Framework\Attributes\TestWith; use stdClass; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -849,7 +850,10 @@ public function testRoutesDontMatchNonMatchingPathsWithLeadingOptionals(): void $this->assertSame('25', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent()); } - public function testCurrentRouteIsResetWhenTheNextRequestDoesNotMatch(): void + #[TestWith(['dispatch'])] + #[TestWith(['dispatchToRoute'])] + #[TestWith(['dispatchToCallback'])] + public function testCurrentRouteIsResetWhenTheNextRequestDoesNotMatch(string $method): void { $router = $this->getRouter(); $router->get('invoices/{invoice}', fn (string $invoice): string => $invoice)->name('invoices.show'); @@ -858,7 +862,11 @@ public function testCurrentRouteIsResetWhenTheNextRequestDoesNotMatch(): void $this->assertSame('invoices.show', $router->currentRouteName()); try { - $router->dispatch(Request::create('missing', 'GET')); + if ($method === 'dispatchToCallback') { + $router->dispatchToCallback(Request::create('missing', 'GET'), static fn (): string => 'unexpected'); + } else { + $router->{$method}(Request::create('missing', 'GET')); + } $this->fail('A NotFoundHttpException should have been thrown.'); } catch (NotFoundHttpException) { From 820c4147b548b63b10be11b0f4f25e973e553791 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:30:20 +0000 Subject: [PATCH 32/36] Return failure when session garbage collection fails Report an error and return a failing command status when the session handler returns false. A zero-row cleanup remains successful, allowing scheduled jobs to distinguish an empty cleanup from a backend failure. Corrects the command ported from laravel/framework#62104 at 13.x revision 5d7797528723107cd4f495326838a7d7d67fbb20. Verified success, zero-row and failure cases in the command test file, full static analysis and formatting. --- .../src/Console/SessionGarbageCollectCommand.php | 12 ++++++++++-- tests/Session/SessionGarbageCollectCommandTest.php | 12 ++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/session/src/Console/SessionGarbageCollectCommand.php b/src/session/src/Console/SessionGarbageCollectCommand.php index 332543b89c..7dce88ea26 100644 --- a/src/session/src/Console/SessionGarbageCollectCommand.php +++ b/src/session/src/Console/SessionGarbageCollectCommand.php @@ -23,13 +23,21 @@ class SessionGarbageCollectCommand extends Command /** * Execute the console command. */ - public function handle(): void + public function handle(): int { - $this->hypervel->make('session') + $result = $this->hypervel->make('session') ->driver() ->getHandler() ->gc($this->hypervel->make('config')->integer('session.lifetime') * 60); + if ($result === false) { + $this->components->error('Session garbage collector failed.'); + + return static::FAILURE; + } + $this->components->info('Session garbage collector ran successfully.'); + + return static::SUCCESS; } } diff --git a/tests/Session/SessionGarbageCollectCommandTest.php b/tests/Session/SessionGarbageCollectCommandTest.php index 531ef571b0..3255675be5 100644 --- a/tests/Session/SessionGarbageCollectCommandTest.php +++ b/tests/Session/SessionGarbageCollectCommandTest.php @@ -8,20 +8,24 @@ use Hypervel\Testbench\Attributes\WithConfig; use Hypervel\Testbench\TestCase; use Mockery as m; +use PHPUnit\Framework\Attributes\TestWith; use SessionHandlerInterface; #[WithConfig('session.driver', 'custom')] #[WithConfig('session.lifetime', 7)] class SessionGarbageCollectCommandTest extends TestCase { - public function testRegisteredCommandCollectsTheConfiguredSessionDriver(): void + #[TestWith([2, 0, 'ran successfully'], 'sessions removed')] + #[TestWith([0, 0, 'ran successfully'], 'nothing expired')] + #[TestWith([false, 1, 'failed'], 'handler failure')] + public function testRegisteredCommandCollectsTheConfiguredSessionDriver(int|false $result, int $exitCode, string $message): void { $handler = m::mock(SessionHandlerInterface::class); - $handler->expects('gc')->with(420)->andReturn(2); + $handler->expects('gc')->with(420)->andReturn($result); $this->app->make(SessionManager::class)->extend('custom', fn () => $handler); $this->artisan('session:garbage-collect') - ->expectsOutputToContain('Session garbage collector ran successfully.') - ->assertSuccessful(); + ->expectsOutputToContain('Session garbage collector ' . $message . '.') + ->assertExitCode($exitCode); } } From 909f1f5a23b50b7a6cbd9f7fb471441342835c0a Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:30:20 +0000 Subject: [PATCH 33/36] Clarify shared after-response settings and commit timing Explain that BusFake forwards after-response settings to the shared dispatcher, affecting every coroutine until restored. Clarify that disabling after-response deferral does not bypass an afterCommit job's transaction boundary. Documents the adaptation of laravel/framework#62130 at 13.x revision 5d7797528723107cd4f495326838a7d7d67fbb20. No runtime behavior changes. Verified the dispatcher flow, full static analysis and formatting. --- src/docs/queues.md | 2 +- src/support/src/Testing/Fakes/BusFake.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/docs/queues.md b/src/docs/queues.md index c9262915f3..3d59e697df 100644 --- a/src/docs/queues.md +++ b/src/docs/queues.md @@ -3843,7 +3843,7 @@ Bus::fake(); Bus::assertNothingDispatched(); ``` -When faking only selected jobs with `Bus::fake([...])`, you may call `Bus::withoutDispatchingAfterResponses()` to run unfaked after-response jobs synchronously during the test. Call `Bus::withDispatchingAfterResponses()` to restore deferred dispatching. +When faking only selected jobs with `Bus::fake([...])`, you may call `Bus::withoutDispatchingAfterResponses()` to run unfaked after-response jobs synchronously during the test. Jobs configured to wait for a database transaction still run only after it commits. Call `Bus::withDispatchingAfterResponses()` to restore deferred dispatching. ### Faking a Subset of Jobs diff --git a/src/support/src/Testing/Fakes/BusFake.php b/src/support/src/Testing/Fakes/BusFake.php index 0193d52f04..b771f001f9 100644 --- a/src/support/src/Testing/Fakes/BusFake.php +++ b/src/support/src/Testing/Fakes/BusFake.php @@ -812,7 +812,7 @@ public function map(array $map): static /** * Allow dispatching after responses. * - * Tests only. Changes the underlying dispatcher's worker-lifetime setting. + * Tests only. Changes the underlying dispatcher for all coroutines until the setting is restored. */ public function withDispatchingAfterResponses(): static { @@ -824,7 +824,7 @@ public function withDispatchingAfterResponses(): static /** * Disable dispatching after responses. * - * Tests only. Changes the underlying dispatcher's worker-lifetime setting. + * Tests only. Changes the underlying dispatcher for all coroutines until the setting is restored. */ public function withoutDispatchingAfterResponses(): static { From 7681730fa4df32acff57089eab14a26803554dff Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:56:56 +0000 Subject: [PATCH 34/36] Preserve numeric AnyOf fields and null wildcard values Restore upstream's associative-rule classification so numeric field maps are not reinterpreted as whole-value rule lists. Sparse numeric arrays are ambiguous; retain the existing contract and document array_values for filtered lists instead of inventing another discriminator. Keep keyed array data intact while wrapping scalar and null values once. This preserves list-item wildcard checks and prevents present null from disappearing before required validation. Empty arrays follow ordinary wildcard semantics. Compared with laravel/framework#62171 at 13.x revision 5d7797528723107cd4f495326838a7d7d67fbb20. Numeric-map and null regressions fail before correction; the complete AnyOf test file, targeted source analysis and formatting pass. --- src/docs/validation.md | 2 ++ src/validation/src/Rules/AnyOf.php | 6 +++--- tests/Validation/ValidationAnyOfRuleTest.php | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/docs/validation.md b/src/docs/validation.md index acfa656ad5..69b4e48ee3 100644 --- a/src/docs/validation.md +++ b/src/docs/validation.md @@ -1678,6 +1678,8 @@ use Hypervel\Validation\Rule; ], ``` +Rulesets with non-sequential keys are treated as maps of fields to rules, so pass filtered rulesets through `array_values`. + #### alpha diff --git a/src/validation/src/Rules/AnyOf.php b/src/validation/src/Rules/AnyOf.php index 87c16aadb0..f4c47c8abb 100644 --- a/src/validation/src/Rules/AnyOf.php +++ b/src/validation/src/Rules/AnyOf.php @@ -36,11 +36,11 @@ public function __construct(array $rules) public function passes(string $attribute, mixed $value): bool { foreach ($this->rules as $rule) { - // Filtered rule lists keep numeric gaps, so only string keys identify a field map. - $keyed = array_any(Arr::wrap($rule), static fn (mixed $definition, int|string $key): bool => is_string($key)); + $keyed = Arr::isAssoc(Arr::wrap($rule)); + // Keyed rules read the value's own fields, including list items matched by wildcards. $validator = Validator::make( - $keyed ? Arr::wrap($value) : [$value], + $keyed && is_array($value) ? $value : [$value], $keyed ? $rule : [$rule], $this->validator->customMessages, // @phpstan-ignore property.notFound $this->validator->customAttributes // @phpstan-ignore property.notFound diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 68b4db9569..781ebe9e65 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -168,14 +168,16 @@ public function testAlternativeRulesPreserveTheirDataShape(array $rules, mixed $ } /** - * Provide filtered rule lists and keyed alternatives. + * Provide keyed alternatives and values with different shapes. */ public static function alternativeRuleShapes(): iterable { - yield 'filtered list rejects invalid email' => [array_filter(['string', null, 'email']), 'not-an-email', false]; - yield 'filtered list accepts valid email' => [array_filter(['string', null, 'email']), 'test@example.com', true]; + yield 'numeric field rejects a scalar' => [[1 => 'array'], [1 => 'invalid'], false]; + yield 'numeric field accepts an array' => [[1 => 'array'], [1 => ['valid']], true]; yield 'wildcard rejects invalid list item' => [['*.email' => 'email'], [['email' => 'invalid']], false]; yield 'wildcard accepts valid list item' => [['*.email' => 'email'], [['email' => 'test@example.com']], true]; + yield 'wildcard rejects a present null' => [['*' => 'required'], null, false]; + yield 'wildcard has no items in an empty array' => [['*' => 'required'], [], true]; yield 'keyed rules reject a scalar missing required fields' => [['email' => 'required|email'], 'invalid', false]; } From 64fcd6c8407279db7b795c4e21e3397c68a5fcda Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:56:56 +0000 Subject: [PATCH 35/36] Reject non-stringable array values in exists and in validation Extend the existing nested-array checks to reject objects that cannot be converted to strings before array_unique or array_diff. Programmatic validation now returns false for these invalid candidates instead of throwing before another AnyOf alternative can run. Preserve valid Stringable values and existing scalar and enum handling. The direct exists and array in cases reproduce conversion errors before correction. The complete Validator test file, targeted source analysis and formatting pass, including Stringable array coverage. --- src/validation/src/Concerns/ValidatesAttributes.php | 6 +++--- tests/Validation/ValidationValidatorTest.php | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/validation/src/Concerns/ValidatesAttributes.php b/src/validation/src/Concerns/ValidatesAttributes.php index 7272406e82..2def0ec913 100644 --- a/src/validation/src/Concerns/ValidatesAttributes.php +++ b/src/validation/src/Concerns/ValidatesAttributes.php @@ -1082,10 +1082,10 @@ public function validateExists(string $attribute, mixed $value, mixed $parameter // that the columns being "verified" shares the given attribute's name. $column = $this->getQueryColumn($parameters, $attribute); - // Nested arrays cannot be compared by array_unique() or bound by whereIn(). + // Nested arrays and non-stringable objects cannot be compared by array_unique(). if (is_array($value)) { foreach ($value as $element) { - if (is_array($element)) { + if (is_array($element) || (is_object($element) && ! $element instanceof Stringable)) { return false; } } @@ -1557,7 +1557,7 @@ public function validateIn(string $attribute, mixed $value, mixed $parameters): { if (is_array($value) && $this->hasRule($attribute, 'Array')) { foreach ($value as $element) { - if (is_array($element)) { + if (is_array($element) || (is_object($element) && ! $element instanceof Stringable)) { return false; } } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 36f7b7307f..9e00666d0d 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -4672,6 +4672,12 @@ public function testValidateIn(): void $v = new Validator($trans, ['name' => ['foo', []]], ['name' => 'Array|In:foo,bar']); $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['name' => [new stdClass]], ['name' => 'Array|In:foo,bar']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['name' => [new Stringable('foo')]], ['name' => 'Array|In:foo,bar']); + $this->assertTrue($v->passes()); } public function testValidateNotIn(): void @@ -5028,6 +5034,9 @@ public function testValidationExists(): void $v = new Validator($trans, ['email' => ['a' => ['foo']]], ['email' => 'exists:users,email_addr']); $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['email' => [new stdClass, new stdClass]], ['email' => 'exists:users,email_addr']); + $this->assertFalse($v->passes()); } public function testValidationExistsIsNotCalledUnnecessarily(): void From 951f902ab98cd48a27e20503cd4b78c9e0d252bc Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:56:57 +0000 Subject: [PATCH 36/36] Reset route state at request dispatch boundaries Keep current-route resetting in dispatch, matching laravel/framework#62123 at 13.x revision 5d7797528723107cd4f495326838a7d7d67fbb20, and mirror it in Hypervel's callback dispatch entrypoint. Do not reset in findRoute: a failed direct dispatchToRoute must preserve the active route as upstream does. Both request entrypoints still clear stale state before Routing listeners, without adding a route stack or extra normal-path context writes. A real nested route test reproduces Router::input failing after a caught missing-route exception before this correction. The complete RoutingRouteTest and RouterExtensionTest files, targeted source analysis and formatting pass. --- src/routing/src/Router.php | 7 ++++--- tests/Routing/RoutingRouteTest.php | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/routing/src/Router.php b/src/routing/src/Router.php index 083b2c774d..265a8e2864 100644 --- a/src/routing/src/Router.php +++ b/src/routing/src/Router.php @@ -610,6 +610,9 @@ public function dispatch(Request $request): SymfonyResponse { CoroutineContext::set(self::CURRENT_REQUEST_CONTEXT_KEY, $request); + // A failed match must not retain an earlier dispatch's route in this coroutine. + CoroutineContext::set(self::CURRENT_ROUTE_CONTEXT_KEY, null); + return $this->dispatchToRoute($request); } @@ -634,6 +637,7 @@ public function dispatchToRoute(Request $request): SymfonyResponse public function dispatchToCallback(Request $request, Closure $callback): SymfonyResponse { CoroutineContext::set(self::CURRENT_REQUEST_CONTEXT_KEY, $request); + CoroutineContext::set(self::CURRENT_ROUTE_CONTEXT_KEY, null); $route = $this->findRoute($request); @@ -657,9 +661,6 @@ public function dispatchToCallback(Request $request, Closure $callback): Symfony */ protected function findRoute(Request $request): Route { - // A failed match must not retain an earlier dispatch's route in this coroutine. - CoroutineContext::set(self::CURRENT_ROUTE_CONTEXT_KEY, null); - if ($this->events->hasListeners(Routing::class)) { $this->events->dispatch(new Routing($request)); } diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index 3b1e0e087f..2d71af0b80 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -851,7 +851,6 @@ public function testRoutesDontMatchNonMatchingPathsWithLeadingOptionals(): void } #[TestWith(['dispatch'])] - #[TestWith(['dispatchToRoute'])] #[TestWith(['dispatchToCallback'])] public function testCurrentRouteIsResetWhenTheNextRequestDoesNotMatch(string $method): void { @@ -877,6 +876,22 @@ public function testCurrentRouteIsResetWhenTheNextRequestDoesNotMatch(string $me $this->assertFalse($router->is('invoices.show')); } + public function testFailedNestedRouteDispatchKeepsTheCurrentRoute(): void + { + $router = $this->getRouter(); + $router->get('invoices/{invoice}', function () use ($router): string { + try { + $router->dispatchToRoute(Request::create('missing', 'GET')); + $this->fail('A NotFoundHttpException should have been thrown.'); + } catch (NotFoundHttpException) { + } + + return $router->input('invoice'); + }); + + $this->assertSame('123', $router->dispatch(Request::create('invoices/123', 'GET'))->getContent()); + } + public function testRoutesDontMatchNonMatchingDomain(): void { $this->expectException(NotFoundHttpException::class);