diff --git a/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md b/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md index 50ec755ac..5dc7de0f4 100644 --- a/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md +++ b/docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md @@ -1079,7 +1079,7 @@ Add one row only for a shared finding or changed lower-level assumption that ano | `queue-14` | `foundation`, `queue` | `foundation` and `queue` (revalidation complete) | `Complete Foundation runtime lifecycles and safe publication`; finding `queue-14` | | `http-03` | `http`, `foundation` | `contracts`, `foundation`, and `http` (revalidation complete) | `Complete Foundation runtime lifecycles and safe publication`; finding `http-03` | | `auth-02` | `auth` | `foundation` and `auth` (revalidation complete) | `Complete Foundation runtime lifecycles and safe publication`; finding `auth-02` | -| `auth-12` | `auth` | `fortify` (revalidation complete); later full `fortify` audit | `Complete Auth correctness, lifecycle, and current parity`; finding `auth-12` | +| `auth-12` | `auth` | `fortify` (revalidation complete) | `Complete Auth correctness, lifecycle, and current parity`; finding `auth-12` | | `database-03` | `database` | `foundation` and `database` (revalidation complete); later full `testbench` audit | `Complete Foundation runtime lifecycles and safe publication`; finding `database-03` | | `foundation-17` | `foundation` | `foundation` and `scout` (revalidation complete) | `Complete Scout current parity, queue, and search lifecycles`; finding `foundation-17` | | `foundation-18` | `foundation` | `foundation` and `scout` (revalidation complete) | `Complete Scout current parity, queue, and search lifecycles`; finding `foundation-18` | @@ -1123,7 +1123,7 @@ Add one row only for a shared finding or changed lower-level assumption that ano | `queue-40` | `queue` | `queue` and `horizon` (revalidation complete) | `Complete Horizon cluster, process, publication, and current Laravel parity`; finding `queue-40` | | `redis-23` | `redis` | `redis` and `horizon` (revalidation complete) | `Complete Horizon cluster, process, publication, and current Laravel parity`; finding `redis-23` | | `telescope-03` | `telescope` | `telescope` (targeted correction complete); later full `telescope` audit | `Complete Horizon cluster, process, publication, and current Laravel parity`; finding `telescope-03` | -| `fortify-01` | `fortify` | `fortify` (targeted correction complete); later full `fortify` audit | `Complete Horizon cluster, process, publication, and current Laravel parity`; finding `fortify-01` | +| `fortify-01` | `fortify` | `fortify` (revalidation complete) | `Complete Horizon cluster, process, publication, and current Laravel parity`; finding `fortify-01` | | `reverb-06` | `reverb` | `reverb` (revalidation complete) | `Complete Horizon cluster, process, publication, and current Laravel parity`; finding `reverb-06` | | `cache-11` | `cache` | `cache` and `reverb` (revalidation complete) | `Complete Reverb connection, shared-state, and current Laravel parity lifecycles`; finding `cache-11` | | `cache-20` | `cache` | `cache` and `reverb` (revalidation complete) | `Complete Reverb connection, shared-state, and current Laravel parity lifecycles`; finding `cache-20` | @@ -1146,7 +1146,7 @@ Add one row only for a shared finding or changed lower-level assumption that ano | `permission-03` | `permission` | `permission` (targeted correction complete); later full `permission` audit | `Harden Eloquent identity and partial-projection safety`; finding `permission-03` | | `permission-04` | `permission` | `permission` (targeted correction complete); later full `permission` audit | `Harden Eloquent identity and partial-projection safety`; finding `permission-04` | | `permission-05` | `permission` | `permission` (targeted correction complete); later full `permission` audit | `Harden Eloquent identity and partial-projection safety`; finding `permission-05` | -| `fortify-02` | `fortify` | `fortify` (targeted correction complete); later full `fortify` audit | `Harden Eloquent identity and partial-projection safety`; finding `fortify-02` | +| `fortify-02` | `fortify` | `fortify` (revalidation complete) | `Harden Eloquent identity and partial-projection safety`; finding `fortify-02` | | `pagination-01` | `pagination` | `pagination` (revalidation complete) | `Complete Pagination correctness, current parity, and query contracts`; finding `pagination-01` | | `pagination-02` | `pagination` | `pagination` (revalidation complete) | `Complete Pagination correctness, current parity, and query contracts`; finding `pagination-02` | | `collections-15` | `collections` | `collections` and `pagination` (revalidation complete) | `Complete Pagination correctness, current parity, and query contracts`; finding `collections-15` | @@ -1308,7 +1308,7 @@ The order is lower-level first where practical. Hypervel has cross-cutting depen - [x] `pagination` - [x] `socialite` - [x] `sanctum` -- [ ] `fortify` +- [x] `fortify` - [ ] `passkeys` - [ ] `permission` - [ ] `jwt` diff --git a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md index 0b6ec4c96..8e8aac57f 100644 --- a/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md +++ b/docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md @@ -1560,7 +1560,7 @@ Append package entries in checklist order. Keep each entry compact but complete - **Design and rejected machinery:** Missing loaded state is checked only at the latest boundary that cannot produce a truthful result without it. Fresh models, loaded SQL nulls, casts/accessors, zero/string-zero, UUIDs, custom keys, compound pivot originals, and Laravel method signatures remain supported. No global strict-mode change, automatic reload, recovery query, registry, synthetic identity, second traversal, new exception type, compatibility branch, state cache, lock, context slot, or consumer-local workaround is added. - **Implementation:** Database now separates keyless objects from stored rows, protects shared persistence and pivot predicates, retains deliberate collection dictionary behavior, and requires truthful soft-delete state. Permission validates cache projections and every model-derived scope, context, target, persisted owner, and deletion-cleanup identity before SQL or side effects; its explicit invalidators use the same fail-closed key identity as tolerant cache reads. Fortify, Pagination, Queue, Scout, Notifications, JSON:API, and Testing reject or contain missing state at their existing owners. Current Laravel JSON:API include and empty-fieldset fixes are ported from their complete originating changes. The invalid Notifications fixture now proves collection-shaped serialization with a restorable ID. Superseded fallback logic, false type claims, malformed assertions, and stale comments are removed. -- **Cross-package revalidation:** Completed Database owns `database-15` through `database-20` and the collection side of `queue-41`; completed Queue owns its single-model side. The completed Notifications audit revalidated queued model wrapping and `notifications-08`, including the public read/unread scopes through the required builder form: the same-named instance predicates shadow static scope dispatch, so `DatabaseNotification::query()->read()` / `unread()` are the working APIs. The full HTTP audit retained `http-04` through `http-06` while completing the broader current-Laravel JSON:API update. Pagination and Scout have completed their full audits and revalidated `pagination-01`, `pagination-02`, `scout-01`, and `scout-02`; Permission, Fortify, and Testing retain targeted findings for their later full audits. The remaining Testing assertion-suite port is recorded without adding temporary runtime behavior. +- **Cross-package revalidation:** Completed Database owns `database-15` through `database-20` and the collection side of `queue-41`; completed Queue owns its single-model side. The completed Notifications audit revalidated queued model wrapping and `notifications-08`, including the public read/unread scopes through the required builder form: the same-named instance predicates shadow static scope dispatch, so `DatabaseNotification::query()->read()` / `unread()` are the working APIs. The full HTTP audit retained `http-04` through `http-06` while completing the broader current-Laravel JSON:API update. Pagination, Scout, and Fortify have completed their full audits and revalidated `pagination-01`, `pagination-02`, `scout-01`, `scout-02`, and `fortify-02`; Permission and Testing retain targeted findings for their later full audits. The remaining Testing assertion-suite port is recorded without adding temporary runtime behavior. - **Regression tests:** Counterfactual coverage spans every key/table/connection operand; partial save/select/delete/refresh/increment paths; original and current Pivot/MorphPivot identities; scalar and model collection comparisons; soft-delete state; Permission cache, guard, positive/negative scope, context, all eleven persisted assignment/removal/sync entry points, reverse APIs, Role/Permission/subject deletion cleanup, soft-delete bypass, and explicit keyless cache invalidation; Fortify challenge and state helpers; every cursor item shape; Queue single/collection publication and notification wrapping; Scout default/custom keys; all four notification state methods; root and relationship JSON:API IDs, includes, and fieldsets; and both Testing assertion surfaces. - **Performance and compatibility:** Ordinary valid paths add bounded local null or raw-attribute checks only. Validation stays inside existing traversals and registrar initialization; no production path gains I/O, an additional collection pass, allocation-heavy machinery, synchronization, yield, or retained state. Laravel public/protected names and signatures remain intact. Behavioral differences are limited to rejecting outputs or mutations that cannot identify, restore, or truthfully describe persisted state, plus current upstream JSON:API fixes. - **Validation and review:** Every changed test file and affected package group is green. The authoritative `composer fix` completed with formatting and both PHPStan configurations clean, followed by the complete parallel components, Testbench, and dogfood suites. Final caller/callee, Laravel API, hot-path, stale-code, and overengineering review is complete. Independent review re-read the full source and test diff, reran the affected gates, and signed off after all corrections were incorporated. @@ -1717,7 +1717,7 @@ Append package entries in checklist order. Keep each entry compact but complete - **Worker and coroutine ownership:** AuthManager, resolved guards, Gate, password brokers, and providers remain worker-lifetime objects. Guard selection, broker overrides, user state, and timed operation state remain coroutine- or operation-local. Event rebinding updates existing owners without resolving unused managers, and retained Eloquent descriptors contain immutable keyspace data rather than provider instances. Broker rebinding and enum normalization complete existing package patterns; the concrete broker check preserves the manager's protected custom-resolution extension point. - **Important rejected concerns:** Do not add a callable parser, unbounded string cache, generic Auth event helper, dispatcher registry, broker reconstruction, listener unregistration, cache-descriptor refcounts, provider ownership objects, settings object, token normalizer, compatibility wrapper, or broad integration-suite clone. Do not mark Recaller sensitive without a demonstrated exception trace, move the reset-link event after custom callbacks, or change AuthorizationException without a supported failing caller. - **Implementation and boundaries:** Token validation, callable reflection, optional event dispatch, event-fake restoration, remember-token cleanup, provider model switching, enum identifiers, configuration ownership, sensitive parameters, metadata, facades, and verification guidance now have one truthful owner. Optional-event guards comply with the repository's required `hasListeners()` convention. Superseded guest-closure and cached-model fields, duplicate top-level defaults, stale types, and false split metadata are removed. -- **Cross-package revalidation:** `auth-01`, `auth-02`, `container-09`, `support-02`, and `cache-04` remain satisfied by the final Auth design. Auth owns `auth-12`; Fortify's two-factor credential path now consumes the shipped top-level hashing default without duplicating it and retains later full-package audit eligibility. The later Sanctum audit completes `auth-18` at Auth's cache owner and consumes Database's named-connection callback correction without adding Auth transaction state. No consumer-local workaround or deferred Auth item remains. +- **Cross-package revalidation:** `auth-01`, `auth-02`, `container-09`, `support-02`, and `cache-04` remain satisfied by the final Auth design. Auth owns `auth-12`; the completed Fortify audit revalidated its two-factor credential path against the shipped top-level hashing default and removed the remaining duplicate Fortify defaults. The later Sanctum audit completes `auth-18` at Auth's cache owner and consumes Database's named-connection callback correction without adding Auth transaction state. No consumer-local workaround or deferred Auth item remains. - **Later Routing revalidation:** `routing-18` revalidates `RedirectIfAuthenticated` against indexed compiled method lookups and removes the suppression made unnecessary by the truthful route-map contract. - **Regression tests:** Deterministic coverage proves hashed and zero-valued tokens, ordered lazy token sources, every callable family, weak-cache cleanup, listener and fake event paths, broker rebinding and restoration, timestamp restoration with original failure identity, old and new model-keyspace invalidation, enum identity and defaulting, config replacement semantics, split metadata, sensitive attributes, real reset wiring, session-dispatcher rebinding, password rehashing, and supported custom-driver callables. - **Performance and complexity:** Ordinary token and enum boundaries add only bounded local type branches; Gate adds one existing cached-listener lookup and avoids unused event allocation and dispatch; password-broker events do the same; cached user lookup keeps one model-string read; and timestamp restoration surrounds an existing database write. No request path gains a lock, retry, extra I/O, serialization layer, network round trip, context slot, container-resolution loop, or unbounded worker-retained state. @@ -1966,3 +1966,27 @@ Append package entries in checklist order. Keep each entry compact but complete - **Performance and complexity:** Ordinary non-Socialite requests are unchanged. Provider construction adds one non-yielding integer increment, request paths add bounded local array/string checks beside existing network work, and JWKS reuse removes repeated network requests while bounding headerless reuse to five minutes by default and retaining one key set and one refresh timestamp per provider. No request path gains a lock, timer, background job, registry, unbounded map, clone, container lookup, serialization layer, or additional ordinary network round trip. - **Validation and review:** Changed tests passed during implementation; focused Socialite, Support, Object Pool, and Reverb coverage, root and split Composer validation, facade and documentation checks, stale-symbol scans, formatting, both PHPStan configurations, the complete parallel components suite, Testbench package mode, dogfood, and `git diff --check` passed. Review independently reproduced the provider-namespace collision, stale config rebinding, nonce-disabled failure, partial-user memoization, and response-state risks, then signed off after every source and plan correction landed. - **Assessment:** Socialite is coroutine-safe, worker-lifecycle-aware, protocol-correct, current at the supported Laravel surface, and first-party extensible without ecosystem-manager machinery. Every accepted finding is fixed at its lowest owner; no stale response state, compatibility workaround, speculative abstraction, unresolved accepted defect, meaningful performance regression, or deferred TODO remains. + +### Complete Fortify correctness, current parity, and configuration contracts + +- **Status and inspected surface:** Complete; implementation, focused validation, the authoritative gate, fresh self-review, and independent code review are signed off. The audit covered Fortify source and tests, Foundation/Auth configuration ownership, package and Boost documentation, published configuration, current Laravel Fortify source and tests, originating changes, and carried `fortify-01`, `fortify-02`, and `auth-12`. The detailed design is recorded in [`2026-08-07-1807-fortify-correctness-current-parity-and-configuration-contracts.md`](2026-08-07-1807-fortify-correctness-current-parity-and-configuration-contracts.md). + +| Findings | Final decision | +|---|---| +| `fortify-03` | Restore ten upstream zero-constructor controllers, `EnsureLoginIsNotThrottled`'s one-parameter constructor, and status-only `PasswordResetResponse` construction while resolving current services at the operation boundary. | +| `fortify-04` | Restore twenty mutable properties and two protected override methods while keeping all six worker-static configuration slots private, typed, and resettable. | +| `fortify-05` | Remove duplicate literals from guaranteed merged configuration reads while retaining defaults only for optional or replace-whole settings. | +| `fortify-06` | Document that feature identifiers are safe reads and that supplying options mutates worker-global configuration at boot. | +| `fortify-07` | Document the named-route or custom reset-URL requirement when Fortify view routes are disabled. | +| `fortify-08` | Give every published configuration group a concise Laravel-style heading. | +| `fortify-09` | Publish lowercase usernames by default and make email verification opt-in for new applications while retaining the package's compatibility fallbacks. | + +- **Architecture and worker ownership:** Fortify's stateless controllers and actions remain worker auto-singletons; request, session, guard, response, and configuration services are resolved at the operation boundary where needed. `Fortify::guardName()` remains the central current-guard authority. The three callback slots, route flag, encrypter, and Hypervel redirect map remain private worker-static configuration owned by typed public methods and the existing exhaustive `flushState()` boundary. OTPHP, chillerlan QR, Passkeys routing, dedicated rate limiting, event gating, transactional recovery-code replacement, and complete two-factor-state validation remain unchanged. +- **Correctness and parity:** Current Laravel zero-constructor, status-only response, mutable property, protected override, published-default, and config-comment surfaces are restored. Required merged config has one owner rather than repeated fallback literals. Feature methods distinguish safe identifier reads from boot-only option mutation. Disabled-view password reset guidance names both supported URL owners. The package README now follows repository order and directs Laravel users from writable static properties to Fortify's typed public configuration methods. +- **Cross-package revalidation:** The Horizon-owned atomic installer finding `fortify-01`, Eloquent-owned complete-state finding `fortify-02`, and Auth-owned config finding `auth-12` remain correct under the final Fortify surface. No new cross-package assumption or consumer-local workaround is introduced. +- **Important rejected concerns:** Do not restore request guards or mutable OTP engine state on worker-lived objects, reopen private static configuration, add optional constructors or magic compatibility properties, recursively merge package configuration, add config validation machinery, change accepted-code replay identity, or introduce a request-scoped Fortify registry, lock, cache, retry, or new context slot. +- **Regression coverage:** Focused API coverage proves all ten controller construction contracts, the exact one-parameter throttle-action constructor, status-only JSON and redirect responses for both view settings with the exact login target, the internal lowercase and email-verification fallbacks, exact protected/public property visibility and mutability, both protected dispatch hooks, and privacy of every worker-static Fortify slot. Route coverage proves that disabling views removes the reset form while retaining both reset submission routes. Installer coverage proves the published lowercase and opt-in email-verification defaults. Existing tests remain authoritative for current guards, static cleanup, Passkeys, two-factor state, OTP replay, events, and atomic publication. +- **Performance and complexity:** Visibility, mutability, stubs, and documentation add no runtime work. Controller response/config resolution remains a bounded in-memory container lookup beside existing authentication, session, hashing, and response work. Removing duplicate defaults adds no work. No path gains database, cache, network, filesystem, lock, retry, yield, serialization, extra coroutine-context, or retained-memory overhead. +- **Laravel-facing result:** Supported Fortify names, signatures, named arguments, construction, response, property, and protected extension APIs are compatible or restored. Deliberate Hypervel differences remain the current-guard authority, private typed worker-static configuration, OTPHP/chillerlan implementations, integrated Passkeys, and worker-safe lifecycle boundaries. +- **Validation and review:** Focused API, route, installer, and complete Fortify coverage is green. The authoritative `composer fix` gate, final stale-state and caller/callee review, and independent code review passed with no remaining finding. +- **Assessment:** The implementation restores current parity and one-source configuration ownership without a compatibility shim, speculative abstraction, hot-path synchronization, or meaningful performance regression. diff --git a/docs/plans/2026-08-07-1807-fortify-correctness-current-parity-and-configuration-contracts.md b/docs/plans/2026-08-07-1807-fortify-correctness-current-parity-and-configuration-contracts.md new file mode 100644 index 000000000..208c1a5f4 --- /dev/null +++ b/docs/plans/2026-08-07-1807-fortify-correctness-current-parity-and-configuration-contracts.md @@ -0,0 +1,490 @@ +# Fortify correctness, current parity, and configuration contracts + +**Status:** Complete; implementation and code review signed off. + +## Objective + +Complete the Fortify audit by restoring current Laravel construction and extension APIs, retaining +Hypervel's deliberate worker-safe guard and static-configuration boundaries, removing duplicated +configuration defaults, and correcting the published configuration and user guidance. + +This work fixes verified defects only. It adds no request state, context slot, lock, cache, retry, +registry, compatibility wrapper, or new configuration mechanism. Existing Hypervel-specific guard, +Passkeys, two-factor, rate-limiter, event, and transaction behavior remains intact. + +## Evidence baseline + +- Hypervel branch baseline: `0.4` at `5ef235766cd0cb4da8cfebfe02f38c552cb6f52a`. +- Prior audited Hypervel snapshot: `db70c7ce7def14382d7d22d2f90b15e8db0ae9d7`. +- Current Laravel Fortify reference: `35037d82e2b28d59729b052c97131e743f9ede74`. +- Current Laravel documentation reference: `9c5a062c14069bab9054b558829e282f9593a065`. +- The only Laravel Fortify change after the prior audit reference is PHPStan 2 support; its complete + changed-file set does not alter the source, config, routes, or tests covered here. +- Laravel Fortify PR `#485` / commit `74cd344` introduced lowercase-user support. It deliberately + kept the internal fallback `false` while publishing `true` for new applications. +- Current Hypervel changes after the audited snapshot were traced. The atomic installer, dedicated + rate limiter, complete two-factor-state checks, guard selection, and related tests remain correct + and must be preserved. +- `fortify-01` and `fortify-02` already identify the atomic installer and complete two-factor-state + corrections. New findings therefore use `fortify-03` through `fortify-09`. + +## Anti-overengineering rules + +The following wording is retained verbatim from the core audit plan. Its principle numbering is +also retained; principles 1–6 remain in the core operating plan. In principle 9, “later in this +plan” refers to that plan's **Established remediation vocabulary** section. + +This audit is not permission to add defensive machinery for every imaginable failure. Do not add an abstraction, state machine, retry loop, configurable timeout, registry, mutex, context slot, cache, or compatibility API merely because it sounds robust. + +Complexity must pay for itself with at least one of: + +- a demonstrated failure; +- a complete source trace proving a realistic vulnerable schedule; +- a clear general capability with real consumers and owner approval; +- deletion of greater or riskier complexity elsewhere. + +Typical Laravel lifecycle semantics define the supported contract. A package that intentionally relies on model events, middleware, listeners, transactions, or another documented mechanism is not defective merely because userland can explicitly bypass that mechanism. Do not build a parallel enforcement path for `withoutEvents()`, raw database writes, disabled middleware, direct transport access, or comparable deliberate bypasses unless the public contract explicitly promises behavior through that bypass. + +Underengineering is equally a failure. Fix every verified defect completely at its lowest owning boundary, never with a partial fix or a local patch over a broken shared contract, and always surface meaningful evidence-backed improvements rather than dropping them to avoid effort. Restraint applies to speculative machinery and cosmetic change, not to complete fixes or worthwhile opportunities. + +Do not treat an upstream difference as a bug without tracing it. Do not treat upstream parity as proof of correctness. A real Hypervel defect remains a defect when Laravel, Hyperf, Symfony, or an SDK has the same hole. + +The audit categories are discovery lenses, not boundaries around what may be corrected. Any genuine issue discovered while auditing, implementing, testing, or reviewing must be investigated, assigned to its lowest owning boundary, and taken through the applicable consensus, implementation, validation, review, and approval workflow—even when it is outside the current package, initial taxonomy, or changed diff. Do not dismiss a verified issue as unrelated or defer it merely to preserve package order. This rule applies only after the evidence threshold is met; it does not turn speculative concerns, deliberate bypasses, unsupported use, or contract violations into work. + +### 7. Preserve hot-path quality + +For every fix, inspect: + +- additional allocations; +- container or facade resolutions; +- locking and atomics; +- hashing and serialization; +- new yields or sleeps; +- retries and polling; +- logging or exception construction; +- retained worker memory; +- cache invalidation and eviction. + +A correctness guard on a cold failure path has a different cost from a new lock or resolver on every request. State the difference explicitly. + +Any proposed change with a measured or source-proven hot-path regression requires explicit owner approval before implementation, even when it fixes a defect. Present the expected frequency and magnitude, the evidence, and the viable alternatives. Do not hide an unavoidable tradeoff inside a general correctness claim. + +Performance improvements must provide a meaningful practical benefit after accounting for code complexity and divergence from upstream. Measure representative behavior where practical. Always surface an evidence-backed opportunity to the owner, but do not implement it without approval; a micro-optimization within measurement noise is neither a reason to diverge nor an actionable finding. + +### 8. Remove superseded design completely + +When a fix changes the owning model, delete obsolete helpers, callbacks, properties, config keys, comments, tests, and documentation. Do not leave a compatibility path or comment describing behavior that no longer exists. Preserve intentional upstream comments unless the new design makes them incorrect. + +### 9. Treat remediation patterns as candidates + +The established patterns later in this plan are a vocabulary, not a lookup table. Choose among per-call parameters, immutable values, scoped bindings, cloning, CoroutineContext, factories, explicit ownership, static reset, or resource teardown only after proving the real lifetime and owner. + +### 10. Reject speculative complexity + +Record low-confidence concerns under rejected or unresolved analysis. Do not implement them. Surface every evidence-backed, meaningful non-defect improvement to the owner with its benefit, cost, and alternatives, then stop for explicit approval. This requirement exists to keep worthwhile opportunities visible, not to discourage finding them. + +## Architecture and retained boundaries + +- Fortify's unbound controllers and actions remain worker auto-singletons only when stateless. + Request, session, and current-guard state is resolved during each operation. +- `Fortify::guardName()` and `Fortify::guard()` remain the current-guard authority. Do not restore + upstream `StatefulGuard` constructor capture on worker-lived objects. +- Fortify's six worker-static configuration slots remain private and typed: three upstream + callback slots, the route flag, the encrypter, and Hypervel's redirect callback map. Public + methods own mutation and access, and `Fortify::flushState()` remains the exhaustive test-reset + boundary. +- Feature identifiers remain safe read-only values at request time. Supplying feature options is + boot-only because it mutates the process-global Config repository. +- Package top-level config defaults are shallow-merged during provider registration. Nested + Passkeys settings remain replaceable and keep their deliberate per-key fallbacks. +- The OTPHP provider retains its worker-safe clock, fresh TOTP objects, generic Cache contract, + configured window, and conservative accepted-code replay identity. +- Fortify owns its integrated polymorphic Passkeys routes. Standalone Passkeys route suppression, + guard/middleware/redirect ownership, and bridge configuration remain unchanged. +- Listener-gated events, transactionally serialized recovery-code replacement, after-commit event + publication, guard-scoped throttling, and complete two-factor-state checks remain unchanged. + +## Findings and decisions + +| ID | Category | Severity | Final decision | +|---|---|---:|---| +| `fortify-03` | Construction API parity | Minor | Remove prohibited DI conversions from ten upstream zero-constructor controllers, restore `EnsureLoginIsNotThrottled`'s one-parameter constructor, and restore status-only `PasswordResetResponse` construction. | +| `fortify-04` | Property and method API parity | Moderate | Restore twenty mutable properties and two protected override methods while retaining necessary private worker-static state. | +| `fortify-05` | Configuration ownership | Minor | Remove duplicate literals from thirteen reads of guaranteed merged config; keep defaults only for genuinely optional/replaced values. | +| `fortify-06` | Worker-lifetime documentation | Minor | Warn on the two public feature methods only when options are supplied. | +| `fortify-07` | Password-reset documentation | Minor | Explain the named-route or custom-URL requirement when view routes are disabled. | +| `fortify-08` | Published configuration | Minor | Add concise Laravel-style headings for every public Fortify config group. | +| `fortify-09` | New-application defaults | Minor | Publish Laravel's intended lowercase and opt-in email-verification defaults while retaining internal fallbacks. | + +## Implementation + +### 1. Restore upstream construction style (`fortify-03`) + +Remove the added constructors and dependency properties from: + +- `ConfirmedPasswordStatusController`; +- `ConfirmedTwoFactorAuthenticationController`; +- `EmailVerificationNotificationController`; +- `EmailVerificationPromptController`; +- `PasswordController`; +- `PasswordResetLinkController`; +- `ProfileInformationController`; +- `RecoveryCodeController`; +- `TwoFactorAuthenticationController`; +- `VerifyEmailController`. + +Also restore `EnsureLoginIsNotThrottled` to its upstream one-parameter constructor by removing the +container property and resolving `LockoutResponse` with `app()` at the response site. + +Resolve response contracts with the existing Laravel-shaped helper at the response site: + +```php +return app(TwoFactorConfirmedResponse::class); + +return app(RedirectAsIntended::class, ['name' => 'email-verification']); +``` + +Preserve the existing contract imports and resolve the contracts, not their concrete response +classes. Parameterized `app()` calls bypass singleton caches but still honor the application's +abstract-to-concrete response binding. + +Resolve typed config only in methods that use it: + +```php +$config = app(Config::class); + +if ($config->boolean('fortify.lowercase_usernames') && $request->has(Fortify::email())) { + // Existing normalization. +} +``` + +Use call-time `app(Config::class)` resolution in `ProfileInformationController` and +`PasswordResetLinkController`, whose constructors are removed. `NewPasswordController`, +`RegisteredUserController`, and `AuthenticatedSessionController` retain their upstream-shaped +constructors and continue using `$this->config`; remove only their duplicate `false` literals. + +Restore `PasswordResetResponse` to status-only construction while combining it with the protected +mutable status correction from step 2: + +```php +public function __construct( + protected string $status, +) { +} + +public function toResponse(Request $request): Response +{ + $views = app(Config::class)->boolean('fortify.views'); + + // Existing JSON/redirect response selection. +} +``` + +Do not remove or replace constructors whose upstream shape injects a guard and whose Hypervel +adaptation deliberately avoids retaining that request guard. Those current-guard differences are +already public and tested. + +### 2. Restore inherited property and override contracts (`fortify-04`) + +Make these fourteen properties `protected` and non-readonly: + +| Class | Property | +|---|---| +| `PrepareAuthenticatedSession` | `$limiter` | +| `EnsureLoginIsNotThrottled` | `$limiter` | +| `AttemptToAuthenticate` | `$limiter` | +| `RedirectIfTwoFactorAuthenticatable` | `$limiter` | +| `EnableTwoFactorAuthentication` | `$provider` | +| `ConfirmTwoFactorAuthentication` | `$provider` | +| `LoginRateLimiter` | `$limiter` | +| `TwoFactorAuthenticationProvider` | `$cache` | +| `FailedPasswordResetLinkRequestResponse` | `$status` | +| `FailedPasswordResetResponse` | `$status` | +| `PasswordResetResponse` | `$status` | +| `SuccessfulPasswordResetLinkRequestResponse` | `$status` | +| `LockoutResponse` | `$limiter` | +| `SimpleViewResponse` | `$view` | + +Use normal promoted properties for the thirteen typed cases: + +```php +public function __construct( + protected LoginRateLimiter $limiter, +) { +} +``` + +`SimpleViewResponse::$view` is the exception: change its existing ordinary untyped property from +private to protected, retain the `callable|string` docblock and constructor-body assignment, and do +not promote it because PHP does not permit `callable` as a property type. Its reflection assertion +checks protected/non-readonly state without expecting a declared type. +`TwoFactorAuthenticationProvider::$clock` remains private readonly: it has no upstream equivalent +and belongs to Hypervel's OTPHP engine architecture. + +Remove `readonly` from these six typed public properties: + +- `RecoveryCodeReplaced::$user` and `$code`; +- `RecoveryCodesGenerated::$user`; +- `PasswordUpdatedViaController::$user`; +- `TwoFactorAuthenticationEvent::$user`; +- `RedirectAsIntended::$name`. + +```php +public function __construct( + public Authenticatable $user, + public string $code, +) { +} +``` + +Restore these methods to `protected`, preserving all Hypervel logic and native types: + +```php +protected function confirmPasswordUsingCustomCallback( + Authenticatable&Model $user, + ?string $password = null, +): bool; + +protected function throttleKey(Request $request): string; +``` + +Keep Fortify's three upstream callback slots, route flag, encrypter, and Hypervel redirect callback +map private. Direct writes cannot remain typed, normalized, and exhaustively reset through the +supported public API. Add one concise README difference directing users from Laravel's writable +static properties to +`authenticateThrough()`, `authenticateUsing()`, `confirmPasswordsUsing()`, `encryptUsing()`, and +`ignoreRoutes()`. + +While touching the README, make its existing content follow the required package order: header, +`Documentation: https://hypervel.org/docs/fortify`, approved public differences, then the upstream +link. Remove the local-path documentation sentence rather than retaining a second documentation +surface. Do not add internal implementation notes. + +Do not add public setters, compatibility properties, magic access, new bindings, or lifecycle +machinery. + +Restore concise upstream constructor title docblocks on every edited constructor that lacks one, +and retain the descriptive `SimpleViewResponse::$view` property annotation. + +### 3. Make merged config the only default owner (`fortify-05`) + +Remove duplicate defaults from all thirteen current read sites: + +```php +return self::config()->string('fortify.username'); +return self::config()->string('fortify.email'); + +return in_array($feature, self::config()->array('fortify.features'), true); + +$lowercaseUsernames = $this->config->boolean('fortify.lowercase_usernames'); + +'prefix' => $config->string('fortify.prefix'), +``` + +In `routes/routes.php`, retain the existing route-file/helper shape and remove only the literals: + +```php +$middleware = (array) config('fortify.middleware'); + +Route::group(['middleware' => $middleware], function () { + $enableViews = config('fortify.views'); + $authMiddleware = config('fortify.auth_middleware'); +}); +``` + +The complete source owners are `Fortify`, `Features`, `FortifyServiceProvider::configureRoutes()`, +the routes file, the five lowercase-username controller paths, and `PasswordResetResponse`. +`hashing.rehash_on_login` is already corrected and is revalidated rather than edited. + +Keep defaults for: + +- `fortify-options.*`, whose whole optional tree may be absent; +- `fortify.limiters.verification`, which is intentionally absent from package config; +- `RoutePath` route-specific values; +- the redirect fallback chain; +- `fortify.passkeys.*`, whose containing array may be replaced by the application. + +Keep nullable `fortify.domain` on `get()`. Do not recursively merge config, create a config DTO, or +add a validation pass. + +### 4. Document conditional feature mutation (`fortify-06`) + +Put the lifecycle boundary on each public method rather than private `setOptions()`: + +```php +/** + * Enable the two factor authentication feature. + * + * Boot-only when options are supplied. Non-empty options mutate the process-global + * configuration repository and affect every subsequent request in the worker; + * calling without options performs no mutation and is safe at request time. + * + * @param array $options + */ +public static function twoFactorAuthentication(array $options = []): string; +``` + +Use equivalent wording on `passkeys()`. Calls without options remain the normal identifier lookup +used by request-time feature checks. Remove the redundant lifecycle paragraph from `setOptions()`. +Do not split the API into separate read/configure methods. + +### 5. Correct the disabled-views guidance (`fortify-07`) + +Keep the Boost guide consistent with the corrected lifecycle and published defaults: supplying +feature options is boot-only while no-argument feature identifiers are safe during request +handling; new published config lowercases usernames by default while the unpublished package +fallback remains false; and new installations opt into email verification after their user model +supports it. + +After the Views paragraph, explain that password-reset notifications still need a URL when +Fortify's view route is disabled. Give the two supported choices and link the customization API to +`/docs/{{version}}/passwords#reset-link-customization`: + +```text +If views are disabled while password resets remain enabled, define a route named +password.reset or configure your frontend reset URL at boot with +ResetPassword::createUrlUsing(). +``` + +Keep detailed reset-notification customization in the existing password documentation; do not +duplicate that guide here. + +### 6. Publish a self-explanatory current config (`fortify-08`, `fortify-09`) + +Add concise Laravel-style comment headings to `src/fortify/stubs/fortify.php` for: + +- guard and route middleware; +- username/email and lowercasing; +- view registration; +- home path and response redirects; +- route prefix/domain and custom paths; +- rate limiters; +- Passkeys bridge settings; +- feature selection. + +Explain only user decisions. Do not copy upstream's removed `passwords` setting, describe worker +internals, or repeat the Boost guide. + +Align only the published new-application defaults with current Laravel: + +```php +'lowercase_usernames' => true, + +'features' => [ + Features::registration(), + Features::resetPasswords(), + // Features::emailVerification(), + Features::updateProfileInformation(), + Features::updatePasswords(), + // Existing Hypervel two-factor and Passkeys options. +], +``` + +Keep `src/fortify/config/fortify.php` at `lowercase_usernames => false` with email verification in +its internal fallback list, matching upstream's package/new-install distinction. + +### 7. Complete canonical records + +- Add the final Fortify section to the audit ledger with findings `fortify-03` through + `fortify-09`, architecture, retained differences, rejected concerns, tests, performance, and + validation. +- Change the existing `auth-12`, `fortify-01`, and `fortify-02` dependency-index wording from a + future Fortify audit to completed revalidation. Add no new dependency row because this work + creates no cross-package assumption. +- Update the prior Auth and Eloquent ledger cross-revalidation sentences so neither retains a + future-Fortify-audit claim; state that this audit revalidated `auth-12` and `fortify-02`. Record + final revalidation of the Horizon-owned `fortify-01` installer finding in Fortify's new section. +- Use the dependency index's established `(revalidation complete)` wording for all three rows. +- Record that restored property mutability changes metadata only: subclasses that store request + state in a response or limiter property own an appropriate scoped or transient binding rather + than mutating the provider's worker-cached contract singleton. +- Mark `fortify` complete in the core checklist. +- Remove the stale `Port Fortify package` entry from `docs/todo.md`. +- Keep this plan as the detailed design reference. Do not add change-history prose to it. + +## Regression coverage + +Create `tests/Fortify/FortifyApiTest.php` for the public construction and extension contracts: + +- prove the ten named controllers have no required constructor; +- prove `EnsureLoginIsNotThrottled` retains its exact one-parameter upstream construction; +- construct `PasswordResetResponse` with only a status and exercise both JSON and redirect + behavior with views disabled and enabled so both call-time Config branches are covered; +- prove the internal package fallback keeps lowercase usernames disabled and email verification + enabled; +- use an exact reflection data provider for fourteen protected mutable and six public mutable + properties; +- subclass `ConfirmPassword` and prove parent invocation dispatches through the protected custom + callback method; +- subclass `LoginRateLimiter` and prove a public operation dispatches through the protected + throttle-key method; +- reflect all six Fortify static slots to prove they remain private, preventing the parity sweep + from accidentally reopening the worker-state boundary. + +Extend `tests/Fortify/Console/InstallCommandTest.php` to load the published config and assert its +semantic defaults: + +```php +$config = require $this->app->configPath('fortify.php'); + +$this->assertTrue($config['lowercase_usernames']); +$this->assertNotContains(Features::emailVerification(), $config['features']); +``` + +Keep existing controller, route, guard, static-state, Passkeys, two-factor, provider, and installer +tests authoritative. Focused verification must include all changed Fortify test files and the +complete `tests/Fortify` group. Documentation comments need no brittle text assertions. + +Extend `FortifyRouteTest` to prove that disabling views removes the named reset form route while +retaining the password-reset submission routes described by the guide. + +Normalize the integrated Passkeys README to the package README order and point it to the public +Fortify documentation instead of the repository-local source path. + +## Performance and compatibility budget + +- Restored property visibility/mutability and method visibility change metadata only; they execute + no extra instruction. +- Controller response resolution keeps the existing per-operation container lookup and adds only + the global container-instance access used by `app()`. Controller Config moves from one + auto-singleton-construction lookup to one singleton lookup per affected operation. These are + bounded local map lookups beside auth/session/hash/response work and add no I/O, allocation + layer, yield, or retained state. +- Removing duplicated config literals does not add work and keeps package config as the sole + default owner. +- Stub, README, Boost, ledger, checklist, and lifecycle-docblock changes have no request cost. +- No database, Cache/Redis, network, filesystem, lock, retry, serialization, coroutine-context, or + event operation is added. +- Laravel zero-constructor, status-only response, mutable property, and protected override APIs are + restored. The only relevant retained API difference is private worker-static configuration with + documented public method alternatives. + +## Rejected concerns and designs + +- Do not rewrite TOTP replay keys around timecodes. An accepted code can match adjacent timecodes; + trying another marker after an atomic add permits duplicate acceptance. The generic Cache + contract has no exact CAS solution, and Redis-only scripting or a package CAS abstraction is + disproportionate. +- Do not restore upstream worker-captured guard fields or Google2FA engine state. +- Do not make stateless classes scoped or add per-request Fortify registries. +- Do not cache custom-authentication callback results or tiny QR mapping work. +- Do not add locks around configuration callbacks or optional events. +- Do not recursively merge nested config or add a configuration validation object. +- Do not add optional constructor dependencies, compatibility branches, magic properties, or + accessors to preserve the flawed Hypervel-only construction/property shapes. +- Do not add a new TOTP secret minimum or alter the current secure default. + +## Verification and review + +1. Run each changed/new Fortify test file immediately after editing. +2. Run the complete Fortify test group after the coherent implementation is complete. +3. Run `composer fix` once as the authoritative formatter, PHPStan, parallel-suite, Testbench, and + dogfood gate. +4. Run `git diff --check` and stale-reference scans for removed constructors, duplicate defaults, + incorrect visibility/readonly modifiers, superseded documentation, and record IDs. +5. Re-read every changed file and trace callers/callees, current guard selection, response binding, + feature config timing, static cleanup, config merge order, Passkeys ownership, and TOTP/event + behavior. +6. Perform a fresh Laravel API, coroutine safety, hot-path, retained-memory, stale-code, and + overengineering review before requesting code review. +7. Complete code-review and final-record review loops before marking the implementation complete. diff --git a/docs/todo.md b/docs/todo.md index 613dd57fb..9f39c5c83 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -3,7 +3,6 @@ ## Authentication - Create hypervel/react-starter-kit. Include the standard skeleton pieces that currently only exist as follow-ups: a `composer dev` script for running the Hypervel development server and frontend asset watcher together, plus explicit Hypervel Vite refresh paths instead of the Laravel plugin's `refresh: true` shortcut. -- Port Fortify package - Port Passport package - Replace permission package fake Passport client-credentials coverage with real Passport tests once Passport is ported. The current tests use a local fake guard/client so the permission package can keep Passport middleware parity without depending on a package that does not exist yet. diff --git a/src/boost/docs/fortify.md b/src/boost/docs/fortify.md index e204053cc..b5797e83d 100644 --- a/src/boost/docs/fortify.md +++ b/src/boost/docs/fortify.md @@ -95,7 +95,7 @@ use Hypervel\Fortify\Features; 'features' => [ Features::registration(), Features::resetPasswords(), - Features::emailVerification(), + // Features::emailVerification(), Features::updateProfileInformation(), Features::updatePasswords(), Features::twoFactorAuthentication([ @@ -110,13 +110,17 @@ use Hypervel\Fortify\Features; ], ``` -`Features::twoFactorAuthentication()` and `Features::passkeys()` store feature options in the config repository while the config file is loaded. Do not call these methods from request handlers. +Email verification is commented out in newly published configuration. Enable it after your user model implements the [`MustVerifyEmail` contract](/docs/{{version}}/fortify#email-verification). + +Supplying options to `Features::twoFactorAuthentication()` or `Features::passkeys()` stores them in the process-global config repository and should only be done during boot. Calling either method without options only returns its feature identifier and is safe during request handling. ### Views Fortify does not ship frontend views, but view routes are enabled by default. If `fortify.views` is `true`, Fortify registers view routes for login, registration, password reset, email verification, password confirmation, and two-factor challenge pages. Register view responses with the methods below, or set `fortify.views` to `false` when your application only uses JSON endpoints. +If views are disabled while password resets remain enabled, define a route named `password.reset` or [customize the password reset URL](/docs/{{version}}/passwords#reset-link-customization) during boot with `ResetPassword::createUrlUsing()`. + Register view responses during boot: ```php @@ -232,7 +236,7 @@ Registration remains application-controlled. Multi-guard applications should mak Fortify's login route accepts the field configured by `fortify.username` and a `password` field. The default username field is `email`. A `remember` field may be provided to use remember-me authentication. -Set `fortify.lowercase_usernames` to `true` if usernames / email addresses should be lowercased during login, registration, profile updates, password reset link requests, and password resets. +The published Fortify configuration lowercases usernames and email addresses during login, registration, profile updates, password reset link requests, and password resets. Set `fortify.lowercase_usernames` to `false` to disable this behavior. Applications without a published `config/fortify.php` file use the package fallback of `false`. ### Customizing User Authentication diff --git a/src/fortify/README.md b/src/fortify/README.md index ee1ce3ecd..0a46a8f77 100644 --- a/src/fortify/README.md +++ b/src/fortify/README.md @@ -1,11 +1,7 @@ Fortify for Hypervel === -Backend controllers and scaffolding for Hypervel authentication. - -Ported from: https://github.com/laravel/fortify - -See `src/boost/docs/fortify.md` for the canonical Fortify and Passkeys documentation. +Documentation: https://hypervel.org/docs/fortify ## Differences From Laravel @@ -22,4 +18,7 @@ See `src/boost/docs/fortify.md` for the canonical Fortify and Passkeys documenta - Fortify caches accepted TOTP codes for the full configured verification window to prevent replay for as long as the code remains acceptable. - Recovery code replacement operates on decoded JSON entries. - 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 tightens loose upstream comparisons and application-model event docs where Hypervel can express the real contract. + +Ported from: https://github.com/laravel/fortify diff --git a/src/fortify/routes/routes.php b/src/fortify/routes/routes.php index 740255527..637ecca74 100644 --- a/src/fortify/routes/routes.php +++ b/src/fortify/routes/routes.php @@ -26,7 +26,7 @@ use Hypervel\Passkeys\Http\Controllers\PasskeyRegistrationController; use Hypervel\Support\Facades\Route; -$middleware = (array) config('fortify.middleware', ['web']); +$middleware = (array) config('fortify.middleware'); $guard = config('fortify.guard'); if (is_string($guard) && $guard !== '') { @@ -34,8 +34,8 @@ } Route::group(['middleware' => $middleware], function () { - $enableViews = config('fortify.views', true); - $authMiddleware = config('fortify.auth_middleware', 'auth'); + $enableViews = config('fortify.views'); + $authMiddleware = config('fortify.auth_middleware'); // Authentication... if ($enableViews) { diff --git a/src/fortify/src/Actions/AttemptToAuthenticate.php b/src/fortify/src/Actions/AttemptToAuthenticate.php index 08ff01ed3..f5354aa27 100644 --- a/src/fortify/src/Actions/AttemptToAuthenticate.php +++ b/src/fortify/src/Actions/AttemptToAuthenticate.php @@ -17,8 +17,11 @@ class AttemptToAuthenticate { use DispatchesEvents; + /** + * Create a new controller instance. + */ public function __construct( - protected readonly LoginRateLimiter $limiter, + protected LoginRateLimiter $limiter, ) { } diff --git a/src/fortify/src/Actions/ConfirmPassword.php b/src/fortify/src/Actions/ConfirmPassword.php index 91aa56f6d..e844d46df 100644 --- a/src/fortify/src/Actions/ConfirmPassword.php +++ b/src/fortify/src/Actions/ConfirmPassword.php @@ -30,7 +30,7 @@ public function __invoke(StatefulGuard $guard, Authenticatable&Model $user, ?str /** * Confirm the user's password using a custom callback. */ - private function confirmPasswordUsingCustomCallback(Authenticatable&Model $user, ?string $password = null): bool + protected function confirmPasswordUsingCustomCallback(Authenticatable&Model $user, ?string $password = null): bool { $callback = Fortify::confirmPasswordsUsingCallback(); diff --git a/src/fortify/src/Actions/ConfirmTwoFactorAuthentication.php b/src/fortify/src/Actions/ConfirmTwoFactorAuthentication.php index 6ee015894..76409e65e 100644 --- a/src/fortify/src/Actions/ConfirmTwoFactorAuthentication.php +++ b/src/fortify/src/Actions/ConfirmTwoFactorAuthentication.php @@ -17,8 +17,11 @@ class ConfirmTwoFactorAuthentication { use DispatchesEvents; + /** + * Create a new action instance. + */ public function __construct( - protected readonly TwoFactorAuthenticationProvider $provider, + protected TwoFactorAuthenticationProvider $provider, ) { } diff --git a/src/fortify/src/Actions/EnableTwoFactorAuthentication.php b/src/fortify/src/Actions/EnableTwoFactorAuthentication.php index 318bc3115..a6612d9f7 100644 --- a/src/fortify/src/Actions/EnableTwoFactorAuthentication.php +++ b/src/fortify/src/Actions/EnableTwoFactorAuthentication.php @@ -18,8 +18,11 @@ class EnableTwoFactorAuthentication { use DispatchesEvents; + /** + * Create a new action instance. + */ public function __construct( - protected readonly TwoFactorAuthenticationProvider $provider, + protected TwoFactorAuthenticationProvider $provider, ) { } diff --git a/src/fortify/src/Actions/EnsureLoginIsNotThrottled.php b/src/fortify/src/Actions/EnsureLoginIsNotThrottled.php index d327ae400..251276d14 100644 --- a/src/fortify/src/Actions/EnsureLoginIsNotThrottled.php +++ b/src/fortify/src/Actions/EnsureLoginIsNotThrottled.php @@ -6,7 +6,6 @@ use Closure; use Hypervel\Auth\Events\Lockout; -use Hypervel\Contracts\Container\Container; use Hypervel\Fortify\Concerns\DispatchesEvents; use Hypervel\Fortify\Contracts\LockoutResponse; use Hypervel\Fortify\LoginRateLimiter; @@ -16,9 +15,11 @@ class EnsureLoginIsNotThrottled { use DispatchesEvents; + /** + * Create a new class instance. + */ public function __construct( - protected readonly LoginRateLimiter $limiter, - protected readonly Container $container, + protected LoginRateLimiter $limiter, ) { } @@ -36,6 +37,6 @@ public function handle(Request $request, Closure $next): mixed static fn (): Lockout => new Lockout($request), ); - return $this->container->make(LockoutResponse::class); + return app(LockoutResponse::class); } } diff --git a/src/fortify/src/Actions/PrepareAuthenticatedSession.php b/src/fortify/src/Actions/PrepareAuthenticatedSession.php index 2548d5f74..0f61802c1 100644 --- a/src/fortify/src/Actions/PrepareAuthenticatedSession.php +++ b/src/fortify/src/Actions/PrepareAuthenticatedSession.php @@ -10,8 +10,11 @@ class PrepareAuthenticatedSession { + /** + * Create a new class instance. + */ public function __construct( - private readonly LoginRateLimiter $limiter, + protected LoginRateLimiter $limiter, ) { } diff --git a/src/fortify/src/Actions/RedirectIfTwoFactorAuthenticatable.php b/src/fortify/src/Actions/RedirectIfTwoFactorAuthenticatable.php index 9208ef5a9..7629182d9 100644 --- a/src/fortify/src/Actions/RedirectIfTwoFactorAuthenticatable.php +++ b/src/fortify/src/Actions/RedirectIfTwoFactorAuthenticatable.php @@ -26,8 +26,11 @@ class RedirectIfTwoFactorAuthenticatable implements RedirectsIfTwoFactorAuthenti { use DispatchesEvents; + /** + * Create a new controller instance. + */ public function __construct( - protected readonly LoginRateLimiter $limiter, + protected LoginRateLimiter $limiter, protected readonly Config $config, ) { } diff --git a/src/fortify/src/Events/PasswordUpdatedViaController.php b/src/fortify/src/Events/PasswordUpdatedViaController.php index ed982c3a7..2319525df 100644 --- a/src/fortify/src/Events/PasswordUpdatedViaController.php +++ b/src/fortify/src/Events/PasswordUpdatedViaController.php @@ -15,7 +15,7 @@ class PasswordUpdatedViaController * Create a new event instance. */ public function __construct( - public readonly Authenticatable $user, + public Authenticatable $user, ) { } } diff --git a/src/fortify/src/Events/RecoveryCodeReplaced.php b/src/fortify/src/Events/RecoveryCodeReplaced.php index 789383552..a9a4f28fc 100644 --- a/src/fortify/src/Events/RecoveryCodeReplaced.php +++ b/src/fortify/src/Events/RecoveryCodeReplaced.php @@ -17,8 +17,8 @@ class RecoveryCodeReplaced * Create a new event instance. */ public function __construct( - public readonly Authenticatable $user, - public readonly string $code, + public Authenticatable $user, + public string $code, ) { } } diff --git a/src/fortify/src/Events/RecoveryCodesGenerated.php b/src/fortify/src/Events/RecoveryCodesGenerated.php index b2edbf4d0..41a5ccebe 100644 --- a/src/fortify/src/Events/RecoveryCodesGenerated.php +++ b/src/fortify/src/Events/RecoveryCodesGenerated.php @@ -15,7 +15,7 @@ class RecoveryCodesGenerated * Create a new event instance. */ public function __construct( - public readonly Authenticatable $user, + public Authenticatable $user, ) { } } diff --git a/src/fortify/src/Events/TwoFactorAuthenticationEvent.php b/src/fortify/src/Events/TwoFactorAuthenticationEvent.php index 05ea83546..5242ad6c7 100644 --- a/src/fortify/src/Events/TwoFactorAuthenticationEvent.php +++ b/src/fortify/src/Events/TwoFactorAuthenticationEvent.php @@ -15,7 +15,7 @@ abstract class TwoFactorAuthenticationEvent * Create a new event instance. */ public function __construct( - public readonly Authenticatable $user, + public Authenticatable $user, ) { } } diff --git a/src/fortify/src/Features.php b/src/fortify/src/Features.php index 2bb6fa060..b4e461d25 100644 --- a/src/fortify/src/Features.php +++ b/src/fortify/src/Features.php @@ -14,7 +14,7 @@ class Features */ public static function enabled(string $feature): bool { - return in_array($feature, self::config()->array('fortify.features', []), true); + return in_array($feature, self::config()->array('fortify.features'), true); } /** @@ -140,6 +140,10 @@ public static function updatePasswords(): string /** * Enable the two factor authentication feature. * + * Boot-only when options are supplied. Non-empty options mutate the process-global + * configuration repository and affect every subsequent request in the worker; + * calling without options performs no mutation and is safe at request time. + * * @param array $options */ public static function twoFactorAuthentication(array $options = []): string @@ -152,6 +156,10 @@ public static function twoFactorAuthentication(array $options = []): string /** * Enable the passkeys feature. * + * Boot-only when options are supplied. Non-empty options mutate the process-global + * configuration repository and affect every subsequent request in the worker; + * calling without options performs no mutation and is safe at request time. + * * @param array $options */ public static function passkeys(array $options = []): string @@ -164,8 +172,6 @@ public static function passkeys(array $options = []): string /** * Set options for the given feature. * - * Boot/config/test only. The config repository is process-global and must not be mutated from request handlers. - * * @param array $options */ private static function setOptions(string $feature, array $options): void diff --git a/src/fortify/src/Fortify.php b/src/fortify/src/Fortify.php index 9b7d7386e..b8f68e48c 100644 --- a/src/fortify/src/Fortify.php +++ b/src/fortify/src/Fortify.php @@ -63,7 +63,7 @@ class Fortify */ public static function username(): string { - return self::config()->string('fortify.username', 'email'); + return self::config()->string('fortify.username'); } /** @@ -71,7 +71,7 @@ public static function username(): string */ public static function email(): string { - return self::config()->string('fortify.email', 'email'); + return self::config()->string('fortify.email'); } /** diff --git a/src/fortify/src/FortifyServiceProvider.php b/src/fortify/src/FortifyServiceProvider.php index adc79ecae..96932695d 100644 --- a/src/fortify/src/FortifyServiceProvider.php +++ b/src/fortify/src/FortifyServiceProvider.php @@ -175,7 +175,7 @@ protected function configureRoutes(): void Route::group([ 'domain' => $config->get('fortify.domain'), - 'prefix' => $config->string('fortify.prefix', ''), + 'prefix' => $config->string('fortify.prefix'), ], function (): void { $this->loadRoutesFrom(__DIR__ . '/../routes/routes.php'); }); diff --git a/src/fortify/src/Http/Controllers/AuthenticatedSessionController.php b/src/fortify/src/Http/Controllers/AuthenticatedSessionController.php index f8b179a24..d254fa02f 100644 --- a/src/fortify/src/Http/Controllers/AuthenticatedSessionController.php +++ b/src/fortify/src/Http/Controllers/AuthenticatedSessionController.php @@ -55,7 +55,7 @@ protected function loginPipeline(LoginRequest $request): Pipeline $customPipeline = Fortify::authenticateThroughCallback(); $configuredPipeline = $this->config->get('fortify.pipelines.login'); $limiter = $this->config->get('fortify.limiters.login'); - $lowercaseUsernames = $this->config->boolean('fortify.lowercase_usernames', false); + $lowercaseUsernames = $this->config->boolean('fortify.lowercase_usernames'); if ($customPipeline !== null) { return (new Pipeline($this->container))->send($request)->through(array_filter($customPipeline($request))); diff --git a/src/fortify/src/Http/Controllers/ConfirmedPasswordStatusController.php b/src/fortify/src/Http/Controllers/ConfirmedPasswordStatusController.php index cab12ce65..3cc33c9b8 100644 --- a/src/fortify/src/Http/Controllers/ConfirmedPasswordStatusController.php +++ b/src/fortify/src/Http/Controllers/ConfirmedPasswordStatusController.php @@ -14,24 +14,17 @@ class ConfirmedPasswordStatusController extends Controller { - /** - * Create a new controller instance. - */ - public function __construct( - private readonly Config $config, - ) { - } - /** * Get the password confirmation status. */ public function show(Request $request): JsonResponse { + $config = app(Config::class); $guard = Fortify::guardName(); $lastConfirmation = (int) $request->session()->get(PasswordConfirmation::sessionKey($guard), 0); $lastConfirmed = Date::now()->unix() - $lastConfirmation; $seconds = $request->has('seconds') ? $request->integer('seconds') : null; - $confirmed = $lastConfirmed < PasswordConfirmation::timeout($this->config, $guard, $seconds); + $confirmed = $lastConfirmed < PasswordConfirmation::timeout($config, $guard, $seconds); return response()->json([ 'confirmed' => $confirmed, diff --git a/src/fortify/src/Http/Controllers/ConfirmedTwoFactorAuthenticationController.php b/src/fortify/src/Http/Controllers/ConfirmedTwoFactorAuthenticationController.php index 1ef6c6d37..375d4657a 100644 --- a/src/fortify/src/Http/Controllers/ConfirmedTwoFactorAuthenticationController.php +++ b/src/fortify/src/Http/Controllers/ConfirmedTwoFactorAuthenticationController.php @@ -5,7 +5,6 @@ namespace Hypervel\Fortify\Http\Controllers; use Hypervel\Contracts\Auth\Authenticatable; -use Hypervel\Contracts\Container\Container; use Hypervel\Database\Eloquent\Model; use Hypervel\Fortify\Actions\ConfirmTwoFactorAuthentication; use Hypervel\Fortify\Contracts\TwoFactorConfirmedResponse; @@ -14,11 +13,6 @@ class ConfirmedTwoFactorAuthenticationController extends Controller { - public function __construct( - private readonly Container $container, - ) { - } - /** * Confirm two factor authentication for the user. */ @@ -29,6 +23,6 @@ public function store(Request $request, ConfirmTwoFactorAuthentication $confirm) $confirm($user, (string) $request->input('code')); - return $this->container->make(TwoFactorConfirmedResponse::class); + return app(TwoFactorConfirmedResponse::class); } } diff --git a/src/fortify/src/Http/Controllers/EmailVerificationNotificationController.php b/src/fortify/src/Http/Controllers/EmailVerificationNotificationController.php index d41688fe4..caa515c99 100644 --- a/src/fortify/src/Http/Controllers/EmailVerificationNotificationController.php +++ b/src/fortify/src/Http/Controllers/EmailVerificationNotificationController.php @@ -5,7 +5,6 @@ namespace Hypervel\Fortify\Http\Controllers; use Hypervel\Contracts\Auth\MustVerifyEmail; -use Hypervel\Contracts\Container\Container; use Hypervel\Fortify\Contracts\EmailVerificationNotificationSentResponse; use Hypervel\Fortify\Http\Responses\RedirectAsIntended; use Hypervel\Http\JsonResponse; @@ -14,11 +13,6 @@ class EmailVerificationNotificationController extends Controller { - public function __construct( - private readonly Container $container, - ) { - } - /** * Send a new email verification notification. */ @@ -30,11 +24,11 @@ public function store(Request $request): mixed if ($user->hasVerifiedEmail()) { return $request->wantsJson() ? new JsonResponse('', 204) - : $this->container->make(RedirectAsIntended::class, ['name' => 'email-verification']); + : app(RedirectAsIntended::class, ['name' => 'email-verification']); } $user->sendEmailVerificationNotification(); - return $this->container->make(EmailVerificationNotificationSentResponse::class); + return app(EmailVerificationNotificationSentResponse::class); } } diff --git a/src/fortify/src/Http/Controllers/EmailVerificationPromptController.php b/src/fortify/src/Http/Controllers/EmailVerificationPromptController.php index be3a12a6c..27aafc748 100644 --- a/src/fortify/src/Http/Controllers/EmailVerificationPromptController.php +++ b/src/fortify/src/Http/Controllers/EmailVerificationPromptController.php @@ -5,7 +5,6 @@ namespace Hypervel\Fortify\Http\Controllers; use Hypervel\Contracts\Auth\MustVerifyEmail; -use Hypervel\Contracts\Container\Container; use Hypervel\Fortify\Contracts\VerifyEmailViewResponse; use Hypervel\Fortify\Http\Responses\RedirectAsIntended; use Hypervel\Http\Request; @@ -13,11 +12,6 @@ class EmailVerificationPromptController extends Controller { - public function __construct( - private readonly Container $container, - ) { - } - /** * Display the email verification prompt. */ @@ -27,7 +21,7 @@ public function __invoke(Request $request): mixed $user = $request->user(); return $user->hasVerifiedEmail() - ? $this->container->make(RedirectAsIntended::class, ['name' => 'email-verification']) - : $this->container->make(VerifyEmailViewResponse::class); + ? app(RedirectAsIntended::class, ['name' => 'email-verification']) + : app(VerifyEmailViewResponse::class); } } diff --git a/src/fortify/src/Http/Controllers/NewPasswordController.php b/src/fortify/src/Http/Controllers/NewPasswordController.php index 288399c8f..b7ff2b83f 100644 --- a/src/fortify/src/Http/Controllers/NewPasswordController.php +++ b/src/fortify/src/Http/Controllers/NewPasswordController.php @@ -44,7 +44,7 @@ public function create(Request $request): ResetPasswordViewResponse */ public function store(Request $request): Responsable { - if ($this->config->boolean('fortify.lowercase_usernames', false) && $request->has(Fortify::email())) { + if ($this->config->boolean('fortify.lowercase_usernames') && $request->has(Fortify::email())) { $request->merge([ Fortify::email() => Str::lower((string) $request->{Fortify::email()}), ]); diff --git a/src/fortify/src/Http/Controllers/PasswordController.php b/src/fortify/src/Http/Controllers/PasswordController.php index 5ef941fd2..6947c3146 100644 --- a/src/fortify/src/Http/Controllers/PasswordController.php +++ b/src/fortify/src/Http/Controllers/PasswordController.php @@ -7,7 +7,6 @@ use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Contracts\Auth\CanResetPassword; use Hypervel\Contracts\Auth\PasswordBroker; -use Hypervel\Contracts\Container\Container; use Hypervel\Fortify\Concerns\DispatchesEvents; use Hypervel\Fortify\Contracts\PasswordUpdateResponse; use Hypervel\Fortify\Contracts\UpdatesUserPasswords; @@ -20,11 +19,6 @@ class PasswordController extends Controller { use DispatchesEvents; - public function __construct( - private readonly Container $container, - ) { - } - /** * Update the user's password. */ @@ -42,7 +36,7 @@ public function update(Request $request, UpdatesUserPasswords $updater): Passwor fn (): PasswordUpdatedViaController => new PasswordUpdatedViaController($user), ); - return $this->container->make(PasswordUpdateResponse::class); + return app(PasswordUpdateResponse::class); } /** diff --git a/src/fortify/src/Http/Controllers/PasswordResetLinkController.php b/src/fortify/src/Http/Controllers/PasswordResetLinkController.php index 80047d35a..d63967afa 100644 --- a/src/fortify/src/Http/Controllers/PasswordResetLinkController.php +++ b/src/fortify/src/Http/Controllers/PasswordResetLinkController.php @@ -6,7 +6,6 @@ use Hypervel\Contracts\Auth\PasswordBroker; use Hypervel\Contracts\Config\Repository as Config; -use Hypervel\Contracts\Container\Container; use Hypervel\Contracts\Support\Responsable; use Hypervel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse; use Hypervel\Fortify\Contracts\RequestPasswordResetLinkViewResponse; @@ -20,18 +19,12 @@ class PasswordResetLinkController extends Controller { - public function __construct( - private readonly Container $container, - private readonly Config $config, - ) { - } - /** * Show the reset password link request view. */ public function create(Request $request): RequestPasswordResetLinkViewResponse { - return $this->container->make(RequestPasswordResetLinkViewResponse::class); + return app(RequestPasswordResetLinkViewResponse::class); } /** @@ -39,7 +32,9 @@ public function create(Request $request): RequestPasswordResetLinkViewResponse */ public function store(SendPasswordResetLinkRequest $request): Responsable { - if ($this->config->boolean('fortify.lowercase_usernames', false) && $request->has(Fortify::email())) { + $config = app(Config::class); + + if ($config->boolean('fortify.lowercase_usernames') && $request->has(Fortify::email())) { $request->merge([ Fortify::email() => Str::lower((string) $request->{Fortify::email()}), ]); @@ -48,8 +43,8 @@ public function store(SendPasswordResetLinkRequest $request): Responsable $status = $this->broker()->sendResetLink($request->only(Fortify::email())); return $status === Password::RESET_LINK_SENT - ? $this->container->make(SuccessfulPasswordResetLinkRequestResponse::class, ['status' => $status]) - : $this->container->make(FailedPasswordResetLinkRequestResponse::class, ['status' => $status]); + ? app(SuccessfulPasswordResetLinkRequestResponse::class, ['status' => $status]) + : app(FailedPasswordResetLinkRequestResponse::class, ['status' => $status]); } /** diff --git a/src/fortify/src/Http/Controllers/ProfileInformationController.php b/src/fortify/src/Http/Controllers/ProfileInformationController.php index a4834d502..d0e4bddcc 100644 --- a/src/fortify/src/Http/Controllers/ProfileInformationController.php +++ b/src/fortify/src/Http/Controllers/ProfileInformationController.php @@ -6,7 +6,6 @@ use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Contracts\Config\Repository as Config; -use Hypervel\Contracts\Container\Container; use Hypervel\Fortify\Contracts\ProfileInformationUpdatedResponse; use Hypervel\Fortify\Contracts\UpdatesUserProfileInformation; use Hypervel\Fortify\Fortify; @@ -16,18 +15,14 @@ class ProfileInformationController extends Controller { - public function __construct( - private readonly Container $container, - private readonly Config $config, - ) { - } - /** * Update the user's profile information. */ public function update(Request $request, UpdatesUserProfileInformation $updater): ProfileInformationUpdatedResponse { - if ($this->config->boolean('fortify.lowercase_usernames', false) && $request->has(Fortify::username())) { + $config = app(Config::class); + + if ($config->boolean('fortify.lowercase_usernames') && $request->has(Fortify::username())) { $request->merge([ Fortify::username() => Str::lower((string) $request->{Fortify::username()}), ]); @@ -38,6 +33,6 @@ public function update(Request $request, UpdatesUserProfileInformation $updater) $updater->update($user, $request->all()); - return $this->container->make(ProfileInformationUpdatedResponse::class); + return app(ProfileInformationUpdatedResponse::class); } } diff --git a/src/fortify/src/Http/Controllers/RecoveryCodeController.php b/src/fortify/src/Http/Controllers/RecoveryCodeController.php index 1bcebc2ce..0e888206a 100644 --- a/src/fortify/src/Http/Controllers/RecoveryCodeController.php +++ b/src/fortify/src/Http/Controllers/RecoveryCodeController.php @@ -5,7 +5,6 @@ namespace Hypervel\Fortify\Http\Controllers; use Hypervel\Contracts\Auth\Authenticatable; -use Hypervel\Contracts\Container\Container; use Hypervel\Database\Eloquent\Model; use Hypervel\Fortify\Actions\GenerateNewRecoveryCodes; use Hypervel\Fortify\Contracts\RecoveryCodesGeneratedResponse; @@ -16,11 +15,6 @@ class RecoveryCodeController extends Controller { - public function __construct( - private readonly Container $container, - ) { - } - /** * Get the two factor authentication recovery codes for authenticated user. * @@ -54,6 +48,6 @@ public function store(Request $request, GenerateNewRecoveryCodes $generate): Rec $generate($user); - return $this->container->make(RecoveryCodesGeneratedResponse::class); + return app(RecoveryCodesGeneratedResponse::class); } } diff --git a/src/fortify/src/Http/Controllers/RegisteredUserController.php b/src/fortify/src/Http/Controllers/RegisteredUserController.php index 6ccd99adf..f6d2e937f 100644 --- a/src/fortify/src/Http/Controllers/RegisteredUserController.php +++ b/src/fortify/src/Http/Controllers/RegisteredUserController.php @@ -39,7 +39,7 @@ public function create(Request $request): RegisterViewResponse */ public function store(Request $request, CreatesNewUsers $creator): RegisterResponse { - if ($this->config->boolean('fortify.lowercase_usernames', false) && $request->has(Fortify::username())) { + if ($this->config->boolean('fortify.lowercase_usernames') && $request->has(Fortify::username())) { $request->merge([ Fortify::username() => Str::lower((string) $request->{Fortify::username()}), ]); diff --git a/src/fortify/src/Http/Controllers/TwoFactorAuthenticationController.php b/src/fortify/src/Http/Controllers/TwoFactorAuthenticationController.php index a10b6399e..832b9b331 100644 --- a/src/fortify/src/Http/Controllers/TwoFactorAuthenticationController.php +++ b/src/fortify/src/Http/Controllers/TwoFactorAuthenticationController.php @@ -5,7 +5,6 @@ namespace Hypervel\Fortify\Http\Controllers; use Hypervel\Contracts\Auth\Authenticatable; -use Hypervel\Contracts\Container\Container; use Hypervel\Database\Eloquent\Model; use Hypervel\Fortify\Actions\DisableTwoFactorAuthentication; use Hypervel\Fortify\Actions\EnableTwoFactorAuthentication; @@ -16,11 +15,6 @@ class TwoFactorAuthenticationController extends Controller { - public function __construct( - private readonly Container $container, - ) { - } - /** * Enable two factor authentication for the user. */ @@ -31,7 +25,7 @@ public function store(Request $request, EnableTwoFactorAuthentication $enable): $enable($user, $request->boolean('force', false)); - return $this->container->make(TwoFactorEnabledResponse::class); + return app(TwoFactorEnabledResponse::class); } /** @@ -44,6 +38,6 @@ public function destroy(Request $request, DisableTwoFactorAuthentication $disabl $disable($user); - return $this->container->make(TwoFactorDisabledResponse::class); + return app(TwoFactorDisabledResponse::class); } } diff --git a/src/fortify/src/Http/Controllers/VerifyEmailController.php b/src/fortify/src/Http/Controllers/VerifyEmailController.php index 38666b215..7ad14b6f6 100644 --- a/src/fortify/src/Http/Controllers/VerifyEmailController.php +++ b/src/fortify/src/Http/Controllers/VerifyEmailController.php @@ -6,7 +6,6 @@ use Hypervel\Auth\Events\Verified; use Hypervel\Contracts\Auth\MustVerifyEmail; -use Hypervel\Contracts\Container\Container; use Hypervel\Fortify\Concerns\DispatchesEvents; use Hypervel\Fortify\Contracts\VerifyEmailResponse; use Hypervel\Fortify\Http\Requests\VerifyEmailRequest; @@ -16,11 +15,6 @@ class VerifyEmailController extends Controller { use DispatchesEvents; - public function __construct( - private readonly Container $container, - ) { - } - /** * Mark the authenticated user's email address as verified. */ @@ -30,7 +24,7 @@ public function __invoke(VerifyEmailRequest $request): VerifyEmailResponse $user = $request->user(); if ($user->hasVerifiedEmail()) { - return $this->container->make(VerifyEmailResponse::class); + return app(VerifyEmailResponse::class); } if ($user->markEmailAsVerified()) { @@ -40,6 +34,6 @@ public function __invoke(VerifyEmailRequest $request): VerifyEmailResponse ); } - return $this->container->make(VerifyEmailResponse::class); + return app(VerifyEmailResponse::class); } } diff --git a/src/fortify/src/Http/Responses/FailedPasswordResetLinkRequestResponse.php b/src/fortify/src/Http/Responses/FailedPasswordResetLinkRequestResponse.php index cc6d92d75..8695e2b46 100644 --- a/src/fortify/src/Http/Responses/FailedPasswordResetLinkRequestResponse.php +++ b/src/fortify/src/Http/Responses/FailedPasswordResetLinkRequestResponse.php @@ -15,7 +15,7 @@ class FailedPasswordResetLinkRequestResponse implements FailedPasswordResetLinkR * Create a new response instance. */ public function __construct( - private readonly string $status, + protected string $status, ) { } diff --git a/src/fortify/src/Http/Responses/FailedPasswordResetResponse.php b/src/fortify/src/Http/Responses/FailedPasswordResetResponse.php index 56033c82e..74a14b0b4 100644 --- a/src/fortify/src/Http/Responses/FailedPasswordResetResponse.php +++ b/src/fortify/src/Http/Responses/FailedPasswordResetResponse.php @@ -15,7 +15,7 @@ class FailedPasswordResetResponse implements FailedPasswordResetResponseContract * Create a new response instance. */ public function __construct( - private readonly string $status, + protected string $status, ) { } diff --git a/src/fortify/src/Http/Responses/LockoutResponse.php b/src/fortify/src/Http/Responses/LockoutResponse.php index 36ef9b9c5..968035072 100644 --- a/src/fortify/src/Http/Responses/LockoutResponse.php +++ b/src/fortify/src/Http/Responses/LockoutResponse.php @@ -13,8 +13,11 @@ class LockoutResponse implements LockoutResponseContract { + /** + * Create a new response instance. + */ public function __construct( - private readonly LoginRateLimiter $limiter, + protected LoginRateLimiter $limiter, ) { } diff --git a/src/fortify/src/Http/Responses/PasswordResetResponse.php b/src/fortify/src/Http/Responses/PasswordResetResponse.php index f3607c935..8806d9175 100644 --- a/src/fortify/src/Http/Responses/PasswordResetResponse.php +++ b/src/fortify/src/Http/Responses/PasswordResetResponse.php @@ -13,9 +13,11 @@ class PasswordResetResponse implements PasswordResetResponseContract { + /** + * Create a new response instance. + */ public function __construct( - private readonly string $status, - private readonly Config $config, + protected string $status, ) { } @@ -24,8 +26,10 @@ public function __construct( */ public function toResponse(Request $request): Response { + $views = app(Config::class)->boolean('fortify.views'); + return $request->wantsJson() ? new JsonResponse(['message' => trans($this->status)], 200) - : redirect(Fortify::redirects('password-reset', $this->config->boolean('fortify.views', true) ? route('login') : null, $request))->with('status', trans($this->status)); + : redirect(Fortify::redirects('password-reset', $views ? route('login') : null, $request))->with('status', trans($this->status)); } } diff --git a/src/fortify/src/Http/Responses/RedirectAsIntended.php b/src/fortify/src/Http/Responses/RedirectAsIntended.php index 34cb3c4a1..f2c85c6e8 100644 --- a/src/fortify/src/Http/Responses/RedirectAsIntended.php +++ b/src/fortify/src/Http/Responses/RedirectAsIntended.php @@ -11,8 +11,11 @@ class RedirectAsIntended implements Responsable { + /** + * Create a new class instance. + */ public function __construct( - public readonly string $name, + public string $name, ) { } diff --git a/src/fortify/src/Http/Responses/SimpleViewResponse.php b/src/fortify/src/Http/Responses/SimpleViewResponse.php index ed4156935..735c20172 100644 --- a/src/fortify/src/Http/Responses/SimpleViewResponse.php +++ b/src/fortify/src/Http/Responses/SimpleViewResponse.php @@ -17,9 +17,16 @@ class SimpleViewResponse implements LoginViewResponse, ResetPasswordViewResponse, RegisterViewResponse, RequestPasswordResetLinkViewResponse, TwoFactorChallengeViewResponse, VerifyEmailViewResponse, ConfirmPasswordViewResponse { - /** @var callable|string */ - private $view; + /** + * The name of the view or the callable used to generate the view. + * + * @var callable|string + */ + protected $view; + /** + * Create a new response instance. + */ public function __construct( callable|string $view, ) { diff --git a/src/fortify/src/Http/Responses/SuccessfulPasswordResetLinkRequestResponse.php b/src/fortify/src/Http/Responses/SuccessfulPasswordResetLinkRequestResponse.php index 780c98ffb..7c459d5ff 100644 --- a/src/fortify/src/Http/Responses/SuccessfulPasswordResetLinkRequestResponse.php +++ b/src/fortify/src/Http/Responses/SuccessfulPasswordResetLinkRequestResponse.php @@ -11,8 +11,11 @@ class SuccessfulPasswordResetLinkRequestResponse implements SuccessfulPasswordResetLinkRequestResponseContract { + /** + * Create a new response instance. + */ public function __construct( - private readonly string $status, + protected string $status, ) { } diff --git a/src/fortify/src/LoginRateLimiter.php b/src/fortify/src/LoginRateLimiter.php index 63dc3d843..e5b33b3f0 100644 --- a/src/fortify/src/LoginRateLimiter.php +++ b/src/fortify/src/LoginRateLimiter.php @@ -15,7 +15,7 @@ class LoginRateLimiter * Create a new login rate limiter instance. */ public function __construct( - private readonly RateLimiter $limiter, + protected RateLimiter $limiter, ) { } @@ -76,7 +76,7 @@ private function limit(Request $request): Limit * Scoped to the current guard so lockouts in one actor silo never * block logins in another for the same username and IP. */ - private function throttleKey(Request $request): string + protected function throttleKey(Request $request): string { return Str::transliterate(Fortify::guardName() . '|' . Str::lower((string) $request->input(Fortify::username())) . '|' . $request->ip()); } diff --git a/src/fortify/src/TwoFactorAuthenticationProvider.php b/src/fortify/src/TwoFactorAuthenticationProvider.php index 0a0de9141..ef3bd0d7c 100644 --- a/src/fortify/src/TwoFactorAuthenticationProvider.php +++ b/src/fortify/src/TwoFactorAuthenticationProvider.php @@ -26,7 +26,7 @@ class TwoFactorAuthenticationProvider implements TwoFactorAuthenticationProvider */ public function __construct( private readonly ClockInterface $clock, - private readonly Repository $cache, + protected Repository $cache, ) { } diff --git a/src/fortify/stubs/fortify.php b/src/fortify/stubs/fortify.php index 29b23dac5..f7a4551c8 100644 --- a/src/fortify/stubs/fortify.php +++ b/src/fortify/stubs/fortify.php @@ -5,31 +5,84 @@ use Hypervel\Fortify\Features; return [ + /* + |-------------------------------------------------------------------------- + | Authentication Guard and Middleware + |-------------------------------------------------------------------------- + | + | These values define the guard and middleware Fortify uses for its routes. + | When null, Fortify follows the current request guard. Set a guard name to + | pin Fortify's built-in routes to that guard. + | + */ + 'middleware' => ['web'], 'guard' => null, 'auth_middleware' => 'auth', + /* + |-------------------------------------------------------------------------- + | Username and Email + |-------------------------------------------------------------------------- + | + | These values identify the login and password-reset fields. Usernames may + | also be normalized to lowercase before authentication or persistence. + | + */ + 'username' => 'email', 'email' => 'email', + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Fortify Views + |-------------------------------------------------------------------------- + | + | Disable view routes when the application provides its own frontend. + | + */ + 'views' => true, + /* + |-------------------------------------------------------------------------- + | Home Path and Redirects + |-------------------------------------------------------------------------- + | + | Fortify uses these destinations after successful authentication actions. + | Null feature redirects fall back to the home path. + | + */ + 'home' => '/home', - 'prefix' => '', + 'redirects' => [ + 'login' => null, + 'logout' => null, + 'password-confirmation' => null, + 'register' => null, + 'email-verification' => null, + 'password-reset' => null, + ], - 'domain' => null, + /* + |-------------------------------------------------------------------------- + | Route Prefix, Domain, and Paths + |-------------------------------------------------------------------------- + | + | Customize Fortify's route group or replace individual route paths here. + | Null path values retain the conventional Fortify path. + | + */ - 'lowercase_usernames' => false, + 'prefix' => '', - 'limiters' => [ - 'login' => 'login', - 'two-factor' => '5,1', - 'passkeys' => 'passkeys', - ], + 'domain' => null, 'paths' => [ 'login' => null, @@ -74,15 +127,30 @@ ], ], - 'redirects' => [ - 'login' => null, - 'logout' => null, - 'password-confirmation' => null, - 'register' => null, - 'email-verification' => null, - 'password-reset' => null, + /* + |-------------------------------------------------------------------------- + | Rate Limiters + |-------------------------------------------------------------------------- + | + | These values select the rate limiters used by Fortify's public endpoints. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => '5,1', + 'passkeys' => 'passkeys', ], + /* + |-------------------------------------------------------------------------- + | Passkeys + |-------------------------------------------------------------------------- + | + | These settings connect Fortify to Hypervel's passkey support. + | + */ + 'passkeys' => [ 'relying_party_id' => env('PASSKEYS_RELYING_PARTY_ID', parse_url(config('app.url'), PHP_URL_HOST)), 'allowed_origins' => env_array('PASSKEYS_ALLOWED_ORIGINS', [config('app.url')]), @@ -90,10 +158,19 @@ 'timeout' => (int) env('PASSKEYS_TIMEOUT', 60000), ], + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Remove features from this array to disable their routes and behavior. + | + */ + 'features' => [ Features::registration(), Features::resetPasswords(), - Features::emailVerification(), + // Features::emailVerification(), Features::updateProfileInformation(), Features::updatePasswords(), Features::twoFactorAuthentication([ diff --git a/src/passkeys/README.md b/src/passkeys/README.md index 172cf59c5..9b5beda16 100644 --- a/src/passkeys/README.md +++ b/src/passkeys/README.md @@ -1,11 +1,7 @@ Passkeys for Hypervel === -Passwordless authentication using WebAuthn passkeys for Hypervel. - -Ported from: https://github.com/laravel/passkeys-server - -See `src/boost/docs/fortify.md` for the canonical Fortify and Passkeys documentation. +Documentation: https://hypervel.org/docs/fortify ## Differences From Laravel @@ -16,3 +12,5 @@ See `src/boost/docs/fortify.md` for the canonical Fortify and Passkeys documenta - Passkeys include explicit orphan cleanup for polymorphic owners. - Passkeys support boot-time request-aware callbacks for redirects and WebAuthn relying party / origin settings, such as for custom domains, multi-guard apps, or multi-tenant apps. - Passkey registration responses do not store request data on singleton response instances. + +Ported from: https://github.com/laravel/passkeys-server diff --git a/tests/Fortify/Console/InstallCommandTest.php b/tests/Fortify/Console/InstallCommandTest.php index 6b02f4e76..a69f9c1d2 100644 --- a/tests/Fortify/Console/InstallCommandTest.php +++ b/tests/Fortify/Console/InstallCommandTest.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Foundation\Application as ApplicationContract; use Hypervel\Filesystem\Filesystem; use Hypervel\Fortify\Console\InstallCommand; +use Hypervel\Fortify\Features; use Hypervel\Fortify\FortifyServiceProvider; use Hypervel\Testbench\TestCase; use Hypervel\Tests\Testing\Fixtures\CleanupActions; @@ -75,6 +76,10 @@ public function testInstallCommandPublishesFortifyResources(): void $this->artisan('fortify:install')->assertSuccessful(); $this->assertFileExists($this->app->configPath('fortify.php')); + $config = require $this->app->configPath('fortify.php'); + + $this->assertTrue($config['lowercase_usernames']); + $this->assertNotContains(Features::emailVerification(), $config['features']); foreach ($this->publishedSupportFiles() as $file) { $this->assertFileExists($file); diff --git a/tests/Fortify/FortifyApiTest.php b/tests/Fortify/FortifyApiTest.php new file mode 100644 index 000000000..b86245c17 --- /dev/null +++ b/tests/Fortify/FortifyApiTest.php @@ -0,0 +1,265 @@ +getConstructor(); + + $this->assertSame(0, $constructor?->getNumberOfRequiredParameters() ?? 0); + } + + /** + * @return array + */ + public static function controllersWithoutRequiredConstructorsProvider(): array + { + return [ + 'confirmed password status' => [ConfirmedPasswordStatusController::class], + 'confirmed two factor authentication' => [ConfirmedTwoFactorAuthenticationController::class], + 'email verification notification' => [EmailVerificationNotificationController::class], + 'email verification prompt' => [EmailVerificationPromptController::class], + 'password update' => [PasswordController::class], + 'password reset link' => [PasswordResetLinkController::class], + 'profile information' => [ProfileInformationController::class], + 'recovery code' => [RecoveryCodeController::class], + 'two factor authentication' => [TwoFactorAuthenticationController::class], + 'verify email' => [VerifyEmailController::class], + ]; + } + + public function testThrottleActionKeepsUpstreamConstructor(): void + { + $constructor = (new ReflectionClass(EnsureLoginIsNotThrottled::class))->getConstructor(); + + $this->assertSame(1, $constructor?->getNumberOfRequiredParameters()); + $this->assertSame(1, $constructor?->getNumberOfParameters()); + $this->assertInstanceOf( + EnsureLoginIsNotThrottled::class, + new EnsureLoginIsNotThrottled($this->app->make(LoginRateLimiter::class)), + ); + } + + public function testPasswordResetResponseUsesStatusOnlyConstructionForJsonAndRedirects(): void + { + $this->app->make(Config::class)->set('fortify.views', false); + $response = new PasswordResetResponse(Password::PASSWORD_RESET); + + $jsonResponse = $response->toResponse(Request::create('/', server: [ + 'HTTP_ACCEPT' => 'application/json', + ])); + + $this->assertInstanceOf(JsonResponse::class, $jsonResponse); + $this->assertSame( + ['message' => trans(Password::PASSWORD_RESET)], + json_decode((string) $jsonResponse->getContent(), true, flags: JSON_THROW_ON_ERROR), + ); + + $redirectResponse = $response->toResponse(Request::create('/')); + + $this->assertInstanceOf(RedirectResponse::class, $redirectResponse); + $this->assertStringEndsWith('/home', $redirectResponse->getTargetUrl()); + + $this->app->make(Config::class)->set('fortify.views', true); + $viewRedirectResponse = $response->toResponse(Request::create('/')); + + $this->assertInstanceOf(RedirectResponse::class, $viewRedirectResponse); + $this->assertSame(route('login'), $viewRedirectResponse->getTargetUrl()); + } + + public function testPackageConfigurationRetainsInternalDefaults(): void + { + $this->assertFalse(config('fortify.lowercase_usernames')); + $this->assertTrue(Features::enabled(Features::emailVerification())); + } + + #[DataProvider('protectedMutablePropertiesProvider')] + public function testProtectedPropertiesRemainMutable(string $class, string $property, ?string $type): void + { + $reflection = new ReflectionProperty($class, $property); + + $this->assertTrue($reflection->isProtected()); + $this->assertFalse($reflection->isReadOnly()); + + if ($type === null) { + $this->assertNull($reflection->getType()); + } else { + $this->assertSame($type, (string) $reflection->getType()); + } + } + + /** + * @return array + */ + public static function protectedMutablePropertiesProvider(): array + { + return [ + 'prepare session limiter' => [PrepareAuthenticatedSession::class, 'limiter', LoginRateLimiter::class], + 'login throttle limiter' => [EnsureLoginIsNotThrottled::class, 'limiter', LoginRateLimiter::class], + 'authentication limiter' => [AttemptToAuthenticate::class, 'limiter', LoginRateLimiter::class], + 'two factor redirect limiter' => [RedirectIfTwoFactorAuthenticatable::class, 'limiter', LoginRateLimiter::class], + 'enable two factor provider' => [EnableTwoFactorAuthentication::class, 'provider', TwoFactorAuthenticationProviderContract::class], + 'confirm two factor provider' => [ConfirmTwoFactorAuthentication::class, 'provider', TwoFactorAuthenticationProviderContract::class], + 'login rate limiter' => [LoginRateLimiter::class, 'limiter', RateLimiter::class], + 'two factor replay cache' => [TwoFactorAuthenticationProvider::class, 'cache', CacheRepository::class], + 'failed reset link status' => [FailedPasswordResetLinkRequestResponse::class, 'status', 'string'], + 'failed reset status' => [FailedPasswordResetResponse::class, 'status', 'string'], + 'reset status' => [PasswordResetResponse::class, 'status', 'string'], + 'successful reset link status' => [SuccessfulPasswordResetLinkRequestResponse::class, 'status', 'string'], + 'lockout limiter' => [LockoutResponse::class, 'limiter', LoginRateLimiter::class], + 'view' => [SimpleViewResponse::class, 'view', null], + ]; + } + + #[DataProvider('publicMutablePropertiesProvider')] + public function testPublicPropertiesRemainMutable(string $class, string $property, string $type): void + { + $reflection = new ReflectionProperty($class, $property); + + $this->assertTrue($reflection->isPublic()); + $this->assertFalse($reflection->isReadOnly()); + $this->assertSame($type, (string) $reflection->getType()); + } + + /** + * @return array + */ + public static function publicMutablePropertiesProvider(): array + { + return [ + 'replaced recovery code user' => [RecoveryCodeReplaced::class, 'user', Authenticatable::class], + 'replacement recovery code' => [RecoveryCodeReplaced::class, 'code', 'string'], + 'generated recovery code user' => [RecoveryCodesGenerated::class, 'user', Authenticatable::class], + 'password updated user' => [PasswordUpdatedViaController::class, 'user', Authenticatable::class], + 'two factor event user' => [TwoFactorAuthenticationEvent::class, 'user', Authenticatable::class], + 'redirect name' => [RedirectAsIntended::class, 'name', 'string'], + ]; + } + + public function testConfirmPasswordDispatchesThroughProtectedCustomCallbackMethod(): void + { + Fortify::confirmPasswordsUsing(static fn (): bool => false); + + $action = new FortifyApiConfirmPassword; + + $this->assertTrue($action(m::mock(StatefulGuard::class), new User, 'secret')); + $this->assertTrue($action->customCallbackInvoked); + } + + public function testLoginRateLimiterDispatchesThroughProtectedThrottleKeyMethod(): void + { + $limiter = new FortifyApiLoginRateLimiter($this->app->make(RateLimiter::class)); + + $limiter->attempts(Request::create('/login')); + + $this->assertTrue($limiter->customThrottleKeyInvoked); + } + + #[DataProvider('privateStaticFortifyPropertiesProvider')] + public function testFortifyStaticConfigurationRemainsPrivate(string $property): void + { + $reflection = new ReflectionProperty(Fortify::class, $property); + + $this->assertTrue($reflection->isPrivate()); + $this->assertTrue($reflection->isStatic()); + } + + /** + * @return array + */ + public static function privateStaticFortifyPropertiesProvider(): array + { + return [ + 'authentication pipeline callback' => ['authenticateThroughCallback'], + 'authentication callback' => ['authenticateUsingCallback'], + 'password confirmation callback' => ['confirmPasswordsUsingCallback'], + 'route registration flag' => ['registersRoutes'], + 'encrypter' => ['encrypter'], + 'redirect callbacks' => ['redirectUsingCallbacks'], + ]; + } +} + +class FortifyApiConfirmPassword extends ConfirmPassword +{ + public bool $customCallbackInvoked = false; + + /** + * Confirm the user's password using a custom callback. + */ + protected function confirmPasswordUsingCustomCallback(Authenticatable&Model $user, ?string $password = null): bool + { + $this->customCallbackInvoked = true; + + return true; + } +} + +class FortifyApiLoginRateLimiter extends LoginRateLimiter +{ + public bool $customThrottleKeyInvoked = false; + + /** + * Get the throttle key for the given request. + */ + protected function throttleKey(Request $request): string + { + $this->customThrottleKeyInvoked = true; + + return 'fortify-api-test'; + } +} diff --git a/tests/Fortify/FortifyRouteTest.php b/tests/Fortify/FortifyRouteTest.php index cbd8d1bf6..b91393e80 100644 --- a/tests/Fortify/FortifyRouteTest.php +++ b/tests/Fortify/FortifyRouteTest.php @@ -109,4 +109,14 @@ public function testNullGuardConfigDoesNotAddGuardSelectionMiddleware(): void $this->assertNotContains('auth.guard:admin', $middleware); $this->assertContains('guest', $middleware); } + + #[WithConfig('fortify.views', false)] + public function testPasswordResetSubmissionRoutesRemainRegisteredWithoutViews(): void + { + $routes = Route::getRoutes(); + + $this->assertNull($routes->getByName('password.reset')); + $this->assertNotNull($routes->getByName('password.email')); + $this->assertNotNull($routes->getByName('password.update')); + } }