Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4078db8
Tighten Fortify controller return types
binaryfire Oct 2, 2026
11fdf22
Remove inaccurate comment from the password rules stub
binaryfire Oct 2, 2026
6079e05
Test that Fortify responses implement their bound contracts
binaryfire Oct 2, 2026
69b874c
Allow customizing Fortify recovery code generation
binaryfire Oct 2, 2026
17f8dba
Keep pending two-factor confirmation on a repeated enable request
binaryfire Oct 2, 2026
2f05c84
Timebox credential checks before the two-factor challenge
binaryfire Oct 2, 2026
d62687b
Test Fortify controllers against real users and services
binaryfire Oct 2, 2026
a075d2d
Record Fortify sync and the Passkeys starting checkpoint
binaryfire Oct 2, 2026
252cd05
Share one session key for passkey verification options
binaryfire Oct 2, 2026
888a512
Restore upstream's passkey lookup signature
binaryfire Oct 2, 2026
3bf2c53
Mark the passkey registration response as transient
binaryfire Oct 2, 2026
fe16d0a
Delete passkeys without repeating the route's ownership check
binaryfire Oct 2, 2026
ab5eece
Stop throttling passkey deletion
binaryfire Oct 2, 2026
d77ce5b
Include the owner's morph class in passkey user handles
binaryfire Oct 2, 2026
cc7f84e
Correct the Passkeys differences from Laravel
binaryfire Oct 2, 2026
9ac4124
Record the owner-scoped passkey route binding
binaryfire Oct 2, 2026
e502e65
Record the Passkeys sync checkpoint
binaryfire Oct 2, 2026
7906320
Report resolved queue names in Horizon migration events
binaryfire Oct 2, 2026
d263851
Restore protected console kernel hooks
binaryfire Oct 2, 2026
ed3ed48
Support placeholder casing in five more validation messages
binaryfire Oct 2, 2026
cf94efc
Compile null-safe column comparisons for every database
binaryfire Oct 2, 2026
82a6476
Complete has-through generic annotations and remove their suppressions
binaryfire Oct 2, 2026
43ba469
Require a stable Sentry SDK release
binaryfire Oct 2, 2026
91cd279
Document float request input retrieval
binaryfire Oct 2, 2026
172de80
Document prohibiting destructive database commands
binaryfire Oct 2, 2026
0ade3ee
Remove historical Hyperf links from package READMEs
binaryfire Oct 2, 2026
1c93a5d
Remove a duplicate Recaller test
binaryfire Oct 2, 2026
e3d4825
Correct the Redis workflow and unsupported cache driver guidance
binaryfire Oct 2, 2026
8954cdf
Resolve every Fortify response binding in the binding test
binaryfire Oct 2, 2026
0fa7667
Remove the Fortify test case's unused database hook
binaryfire Oct 2, 2026
921ef94
Document recovery code generator boot rule and request number defaults
binaryfire Oct 2, 2026
15bb9f3
Make two Passkeys test names and a kernel test docblock accurate
binaryfire Oct 2, 2026
67a09c6
Store pending passkey options per guard
binaryfire Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ Each integration group has its own workflow file in `.github/workflows/`:
|----------|------|-----------|
| `engine.yml` | HTTP test servers | `tests/Integration/Engine`, `tests/Integration/HttpServer` |
| `databases.yml` | MySQL, MariaDB, PostgreSQL, SQLite | `tests/Integration/Database`, `tests/Integration/*/Database/*` |
| `redis.yml` | Redis, Redis Cluster, Valkey | `tests/Integration/Auth/Redis`, `tests/Integration/Broadcasting/Redis`, `tests/Integration/Cache/Redis`, `tests/Integration/Horizon`, `tests/Integration/Http/Redis`, `tests/Integration/Queue/Redis`, `tests/Integration/RateLimiter/Redis`, `tests/Integration/Redis`, `tests/Integration/Session/Redis`; it also reruns the driver-neutral queue chaining and dispatching tests with Redis, the listed topology-neutral Reverb state tests with Cluster, and Reverb state recovery with Valkey |
| `redis.yml` | Redis, Redis Cluster, Valkey | `tests/Integration/Auth/Redis`, `tests/Integration/Broadcasting/Redis`, `tests/Integration/Cache`, `tests/Integration/Horizon`, `tests/Integration/Http/Redis`, `tests/Integration/OpenTelemetry/Redis`, `tests/Integration/Queue`, `tests/Integration/RateLimiter/Redis`, `tests/Integration/Redis`, `tests/Integration/Session/Redis`; Cache and Queue run with Redis selected, and it also reruns the listed topology-neutral Reverb state tests with Cluster and Reverb state recovery with Valkey |
| `reverb.yml` | Redis-backed Reverb servers and state | `tests/Integration/Reverb` |
| `scout.yml` | Meilisearch, Typesense | `tests/Integration/Scout/*` |

Expand Down Expand Up @@ -929,7 +929,7 @@ Update upstream test imports to point at the new Fixtures namespace.
Tests for these features should be **removed** (not commented out) without asking — they will never be supported:

- **Databases:** SQL Server, MongoDB, DynamoDB — Hypervel only supports MySQL, MariaDB, PostgreSQL, and SQLite
- **Cache drivers:** Memcached, DynamoDB, MongoDB
- **Cache drivers:** Memcached, APC / APCu, DynamoDB, MongoDB
- **Dynamic connections:** `DB::build()`, `DB::connectUsing()` — incompatible with Swoole connection pooling
- **Container access:** ArrayAccess and dynamic service properties

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"psr/log": "^3.0",
"psr/simple-cache": "^3.0",
"psy/psysh": "dev-main",
"sentry/sentry": "dev-master",
"sentry/sentry": "^4.32",
"spomky-labs/otphp": "^11.0",
"symfony/console": "^8.1.2",
"symfony/dom-crawler": "^8.1",
Expand Down
12 changes: 6 additions & 6 deletions docs/upstream-sync/sync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ laravel/facade-documenter:

laravel/fortify:
branch: 1.x
checked_through: null
last_reviewed_pr: null
sync_date: null
checked_through: c456642584c102f6a6aafee9932299542da637b5
last_reviewed_pr: 703
sync_date: '2026-10-02'

laravel/passkeys-server:
branch: main
checked_through: null
last_reviewed_pr: null
sync_date: null
checked_through: 4f81dfd5f7f983bdfe3ee6b3971c51acaa7844c3
last_reviewed_pr: 40
sync_date: '2026-10-02'
notes: Composer package is laravel/passkeys.

laravel/sanctum:
Expand Down
6 changes: 0 additions & 6 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,6 @@ parameters:
- '#Call to an undefined method TModel of Hypervel\\Database\\Eloquent\\Model::#'
- '#Call to an undefined method TRelatedModel of Hypervel\\Database\\Eloquent\\Model::#'

# Deep generic template limitations - PHPStan can't fully resolve complex generic type relationships
- identifier: generics.lessTypes
path: src/database/*
- identifier: generics.notSubtype
path: src/database/*

# Covariant template types in invariant/contravariant positions - Laravel uses @template-covariant
# for semantic documentation on collection-like classes even though it violates strict variance rules.
# The code is functionally correct; this is a PHPStan limitation with PHP's lack of runtime generics.
Expand Down
2 changes: 0 additions & 2 deletions src/context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ Context for Hypervel
===

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/context)

Ported from: https://github.com/hyperf/context
5 changes: 1 addition & 4 deletions src/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,4 @@ Contracts for Hypervel

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/contracts)

Ported from:

- https://github.com/laravel/framework/tree/13.x/src/Illuminate/Contracts
- https://github.com/hyperf/contract
Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Contracts
15 changes: 0 additions & 15 deletions src/contracts/src/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,16 @@ public function handle(InputInterface $input, ?OutputInterface $output = null):
*/
public function bootstrap(): void;

/**
* Define the application's command schedule.
*/
public function schedule(Schedule $schedule): void;

/**
* Resolve a console schedule instance.
*/
public function resolveConsoleSchedule(): Schedule;

/**
* Register the commands for the application.
*/
public function commands(): void;

/**
* Register a Closure based command with the application.
*/
public function command(string $signature, Closure $callback): ClosureCommand;

/**
* Add loadPaths in the given directory.
*/
public function load(array|string $paths): void;

/**
* Set the Artisan commands provided by the application.
*
Expand Down
2 changes: 0 additions & 2 deletions src/coordinator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ Coordinator for Hypervel
===

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/coordinator)

Ported from: https://github.com/hyperf/hyperf
2 changes: 0 additions & 2 deletions src/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ Framework for Hypervel
===

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/framework)

Ported from: https://github.com/hyperf/hyperf/tree/master/src/framework
2 changes: 0 additions & 2 deletions src/coroutine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ Coroutine for Hypervel
===

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/coroutine)

Ported from: https://github.com/hyperf/coroutine
2 changes: 1 addition & 1 deletion src/database/src/Eloquent/Concerns/HasRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ protected function guessBelongsToRelation(): string
* @param HasMany<TIntermediateModel, covariant $this>|HasOne<TIntermediateModel, covariant $this>|string $relationship
* @return (
* $relationship is string
* ? PendingHasThroughRelationship<Model, $this>
* ? PendingHasThroughRelationship<Model, $this, HasOneOrMany<Model, $this, *>>
* : (
* $relationship is HasMany<TIntermediateModel, $this>
* ? PendingHasThroughRelationship<TIntermediateModel, $this, HasMany<TIntermediateModel, $this>>
Expand Down
4 changes: 2 additions & 2 deletions src/database/src/Eloquent/PendingHasThroughRelationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/**
* @template TIntermediateModel of Model
* @template TDeclaringModel of Model
* @template TLocalRelationship of HasOneOrMany<TIntermediateModel, TDeclaringModel>
* @template TLocalRelationship of HasOneOrMany<TIntermediateModel, TDeclaringModel, *>
*/
class PendingHasThroughRelationship
{
Expand Down Expand Up @@ -52,7 +52,7 @@ public function __construct(Model $rootModel, HasOneOrMany $localRelationship)
*
* @template TRelatedModel of Model
*
* @param (callable(TIntermediateModel): (HasMany<TRelatedModel, TIntermediateModel>|HasOne<TRelatedModel, TIntermediateModel>|MorphOneOrMany<TRelatedModel, TIntermediateModel>))|string $callback
* @param (callable(TIntermediateModel): (HasMany<TRelatedModel, TIntermediateModel>|HasOne<TRelatedModel, TIntermediateModel>|MorphOneOrMany<TRelatedModel, TIntermediateModel, *>))|string $callback
* @return (
* $callback is string
* ? HasManyThrough<Model, TIntermediateModel, TDeclaringModel>|HasOneThrough<Model, TIntermediateModel, TDeclaringModel>
Expand Down
7 changes: 7 additions & 0 deletions src/database/src/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,13 @@ protected function dateBasedWhere(string $type, Builder $query, array $where): s
*/
protected function whereColumn(Builder $query, array $where): string
{
if ($where['operator'] === '<=>') {
$where['column'] = $where['first'];
$where['value'] = new QueryExpression($this->wrap($where['second']));

return $this->whereNullSafeEquals($query, $where);
}

$operator = str_replace('?', '??', $where['operator']);

return $this->wrap($where['first']) . ' ' . $operator . ' ' . $this->wrap($where['second']);
Expand Down
42 changes: 40 additions & 2 deletions src/docs/fortify.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,18 @@ The built-in two-factor routes include:

Fortify stores recovery codes as one encrypted JSON value. When a recovery code is used, Hypervel Fortify replaces the exact decoded JSON array entry and re-encrypts the whole JSON value.

You may customize how recovery codes are generated by registering a callback in the `boot` method of your application's `App\Providers\FortifyServiceProvider` class. Fortify invokes the callback once for each recovery code it generates:
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

```php
use Hypervel\Fortify\Fortify;
use Hypervel\Support\Str;

public function boot(): void
{
Fortify::generateRecoveryCodesUsing(fn (): string => Str::upper(Str::random(16)));
}
```

The two-factor provider defaults to 32-character TOTP secrets. The optional `window` feature option is step-based: a value of `1` accepts the previous, current, and next 30-second periods. Accepted TOTP codes are cached for the full accepted window to prevent replay for as long as Fortify still accepts the code. Hypervel Fortify uses fresh OTPHP TOTP objects with an injected clock, so verification does not mutate shared TOTP engine state in a Swoole worker.

During login, users with enabled two-factor authentication are redirected to the two-factor challenge route. JSON login requests receive a response containing a `two_factor` boolean. The challenge form should submit either a `code` field containing a TOTP code or a `recovery_code` field containing one of the user's recovery codes to `POST /two-factor-challenge`.
Expand Down Expand Up @@ -500,6 +512,8 @@ The resolved relying party ID must be a registrable-domain suffix of the resolve

`user_handle_secret` is a long-lived, nonempty secret used to derive stable WebAuthn user handles. It defaults to the app key for convenience, but production applications should set a dedicated value before registering passkeys. Changing it changes generated user handles.

Each user handle is derived from the owner's morph type, table, and primary key. If one relying party serves several tenant databases whose owners can share the same type and key, override `getPasskeyUserHandle()` on your passkey user model to include a stable tenant identifier that does not reveal personal information.

<a name="frontend-package"></a>
### Frontend Package

Expand All @@ -509,6 +523,15 @@ Hypervel's passkey routes are compatible with the `@laravel/passkeys` frontend p
npm install @laravel/passkeys
```

Then, you may initiate passkey registration and verification from your frontend:

```js
import { Passkeys } from '@laravel/passkeys';

await Passkeys.register({ name: 'MacBook Pro' });
await Passkeys.verify();
```

The frontend package defaults to these backend endpoints:

<div class="content-list" markdown="1">
Expand All @@ -520,7 +543,7 @@ The frontend package defaults to these backend endpoints:

</div>

It also supports route overrides:
If your application uses custom passkey endpoint URIs, you may override the routes on a per-call basis:

```js
await Passkeys.verify({
Expand All @@ -529,8 +552,18 @@ await Passkeys.verify({
submit: '/passkeys/confirm',
},
});

await Passkeys.register({
name: 'MacBook Pro',
routes: {
options: '/user/passkeys/options',
submit: '/user/passkeys',
},
});
```

The package also provides React, Vue, and Svelte helpers via `@laravel/passkeys/react`, `@laravel/passkeys/vue`, and `@laravel/passkeys/svelte`.

<a name="request-and-response-contracts"></a>
### Request And Response Contracts

Expand All @@ -548,6 +581,10 @@ Custom passkey frontends should use JSON requests and preserve the normal Hyperv

</div>

Standard requests receive redirects instead. Login and confirmation redirect to the intended destination, while registration and deletion redirect back with a `passkey-registered` or `passkey-deleted` status in the session.

A successful passkey confirmation marks the session as password confirmed for the current guard, so it satisfies that guard's `password.confirm` middleware.

<a name="customizing-passkeys"></a>
### Customizing Passkeys

Expand Down Expand Up @@ -742,7 +779,7 @@ Standalone routes use the following configuration options:
| `guard` | The guard selected for standalone routes. An omitted or null value uses the current request guard. |
| `middleware` | The required middleware applied to every standalone route. |
| `management_middleware` | The required additional middleware applied when creating or deleting passkeys. |
| `throttle` | The throttle middleware applied to passkey login, confirmation, registration, and deletion endpoints. Omission uses `throttle:6,1`; null disables throttling. |
| `throttle` | The throttle middleware applied to passkey login, confirmation, and registration endpoints. Omission uses `throttle:6,1`; null disables throttling. |
| `redirect` | The successful login destination used when no intended URL exists. Omission uses `/`. |

Call `Passkeys::ignoreRoutes()` during boot before registering your own endpoints:
Expand Down Expand Up @@ -775,6 +812,7 @@ Call these only during application boot or tests:
- `Fortify::updateUserProfileInformationUsing()`
- `Fortify::updateUserPasswordsUsing()`
- `Fortify::resetUserPasswordsUsing()`
- `Fortify::generateRecoveryCodesUsing()`
- `Fortify::redirectUserForTwoFactorAuthenticationUsing()`
- `Features::twoFactorAuthentication($options)`
- `Features::passkeys($options)`
Expand Down
8 changes: 8 additions & 0 deletions src/docs/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ Some migration operations are destructive, which means they may cause you to los
php artisan migrate --force
```

To prevent destructive database commands from running in production at all, even with `--force`, call the `DB` facade's `prohibitDestructiveCommands` method from the `boot` method of your application's `AppServiceProvider`. This prohibits the `db:wipe`, `migrate:fresh`, `migrate:refresh`, `migrate:reset`, and `migrate:rollback` commands:

```php
use Hypervel\Support\Facades\DB;

DB::prohibitDestructiveCommands($this->app->isProduction());
```

<a name="rolling-back-migrations"></a>
### Rolling Back Migrations

Expand Down
8 changes: 8 additions & 0 deletions src/docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,14 @@ $users = DB::table('users')
->get();
```

The `<=>` operator compares two columns while treating two `NULL` values as equal, on every supported database:

```php
$users = DB::table('users')
->whereColumn('billing_email', '<=>', 'email')
->get();
```

You may also pass an array of column comparisons to the `whereColumn` method. These conditions will be joined using the `and` operator:

```php
Expand Down
11 changes: 10 additions & 1 deletion src/docs/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,12 +417,21 @@ $name = $request->string('name')->trim();
<a name="retrieving-integer-input-values"></a>
#### Retrieving Integer Input Values

To retrieve input values as integers, you may use the `integer` method. This method casts the input value to an integer. If the input is not present, it will return the default value you specify. This is particularly useful for pagination or other numeric inputs:
To retrieve input values as integers, you may use the `integer` method. This method casts the input value to an integer. If the input is not present, it will return the default value you specify, or `0` if you don't specify one. This is particularly useful for pagination or other numeric inputs:

```php
$perPage = $request->integer('per_page');
```

<a name="retrieving-float-input-values"></a>
#### Retrieving Float Input Values

Similarly, the `float` method retrieves an input value as a float. If the input is not present, it will return the default value you specify, or `0.0` if you don't specify one:

```php
$price = $request->float('price');
```

<a name="retrieving-clamped-input-values"></a>
#### Retrieving Clamped Input Values

Expand Down
2 changes: 0 additions & 2 deletions src/engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ Engine for Hypervel

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/engine)

Ported from: https://github.com/hyperf/engine

## Architecture

Hypervel supports Swoole only. Its HTTP, WebSocket, and Reverb servers use the Swoole-specific request and response bridges from `hypervel/http-server`; Hyperf's interchangeable Swoole/Swow HTTP server and response-emitter portability layer is intentionally not part of Engine.
2 changes: 1 addition & 1 deletion src/fortify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ Documentation: https://hypervel.org/docs/fortify
- Fortify's two-factor provider uses OTPHP instead of Google2FA and generates 32-character secrets by default.
- Two-factor user models implement the `TwoFactorAuthenticationUser` contract as well as using the `TwoFactorAuthenticatable` trait. Recovery codes are consumed atomically through the user's `consumeRecoveryCode()` method; `TwoFactorLoginRequest::validRecoveryCode()` only checks a code and leaves the login challenge in the session.
- Fortify omits Laravel's deprecated `Rules\Password`.
- Fortify keeps Laravel's directly writable static configuration properties private so worker-lifetime state remains typed and resettable. Use `authenticateThrough()`, `authenticateUsing()`, `confirmPasswordsUsing()`, `encryptUsing()`, and `ignoreRoutes()` instead.
- Fortify keeps Laravel's directly writable static configuration properties private so worker-lifetime state remains typed and resettable. Use `authenticateThrough()`, `authenticateUsing()`, `confirmPasswordsUsing()`, `generateRecoveryCodesUsing()`, `encryptUsing()`, and `ignoreRoutes()` instead.

Ported from: https://github.com/laravel/fortify
2 changes: 1 addition & 1 deletion src/fortify/routes/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@
->name('passkey.store');

Route::delete(RoutePath::for('passkey.destroy', '/user/passkeys/{passkey}'), [PasskeyRegistrationController::class, 'destroy'])
->middleware($passkeyManageMiddleware)
->middleware($passkeyMiddleware)
->name('passkey.destroy');
}
});
27 changes: 17 additions & 10 deletions src/fortify/src/Actions/RedirectIfTwoFactorAuthenticatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Hypervel\Fortify\LoginRateLimiter;
use Hypervel\Fortify\TwoFactorAuthenticatable;
use Hypervel\Http\Request;
use Hypervel\Support\Timebox;
use Hypervel\Validation\ValidationException;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
Expand Down Expand Up @@ -75,21 +76,27 @@ protected function validateCredentials(Request $request): Authenticatable&Model
}

$provider = $this->provider();
$user = $provider->retrieveByCredentials($request->only(Fortify::username(), 'password'));

$password = $request->input('password');
return (new Timebox)->call(function (Timebox $timebox) use ($request, $provider): Authenticatable&Model {
$user = $provider->retrieveByCredentials($request->only(Fortify::username(), 'password'));

if (! $user instanceof Authenticatable || ! $user instanceof Model || ! $provider->validateCredentials($user, ['password' => $password])) {
$this->fireFailedEvent($request, $user);
$password = $request->input('password');

$this->throwFailedAuthenticationException($request);
}
if (! $user instanceof Authenticatable || ! $user instanceof Model || ! $provider->validateCredentials($user, ['password' => $password])) {
$this->fireFailedEvent($request, $user);

if ($this->config->boolean('hashing.rehash_on_login')) {
$provider->rehashPasswordIfRequired($user, ['password' => $password]);
}
$this->throwFailedAuthenticationException($request);
}

return $user;
if ($this->config->boolean('hashing.rehash_on_login')) {
$provider->rehashPasswordIfRequired($user, ['password' => $password]);
}

// Only failed attempts need a fixed duration, so successful logins skip the wait, as SessionGuard does.
$timebox->returnEarly();

return $user;
}, $this->config->integer('auth.timebox_duration'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Config::integer() throws InvalidArgumentException when the key is missing instead of returning a fallback (src/config/src/Repository.php: if (! is_int($value)) { throw ... }). If an application publishes or overrides config/auth.php without timebox_duration (e.g., a config from before this key existed in src/foundation/config/auth.php), every two-factor login attempt 500s at this added line, whereas get() would otherwise degrade gracefully. Use get() with a fallback and an explicit cast so a missing key maps to the framework default (200000) instead of failing authentication.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/fortify/src/Actions/RedirectIfTwoFactorAuthenticatable.php, line 99:

<comment>`Config::integer()` throws `InvalidArgumentException` when the key is missing instead of returning a fallback (src/config/src/Repository.php: `if (! is_int($value)) { throw ... }`). If an application publishes or overrides `config/auth.php` without `timebox_duration` (e.g., a config from before this key existed in `src/foundation/config/auth.php`), every two-factor login attempt 500s at this added line, whereas `get()` would otherwise degrade gracefully. Use `get()` with a fallback and an explicit cast so a missing key maps to the framework default (200000) instead of failing authentication.</comment>

<file context>
@@ -75,21 +76,27 @@ protected function validateCredentials(Request $request): Authenticatable&Model
+            $timebox->returnEarly();
+
+            return $user;
+        }, $this->config->integer('auth.timebox_duration'));
     }
 
</file context>
Suggested change
}, $this->config->integer('auth.timebox_duration'));
}, (int) $this->config->get('auth.timebox_duration', 200000));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. auth.timebox_duration is a required setting that ships in the framework's auth.php, and framework config is shallow-merged with the application's, so a published auth.php without the key still gets the default. AuthManager and PasswordBrokerManager read it with integer() too. If a required key is missing, it should fail loudly rather than quietly fall back.

}

/**
Expand Down
Loading
Loading