diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 23a580db25..723d70a989 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -131,3 +131,15 @@ jobs: - name: Type-check generated Wayfinder output run: pnpm typecheck:wayfinder + + # Telescope's dashboard is a standalone npm package whose devEngines require npm 11.10 or newer. + - name: Install npm for Telescope + run: npm install --global npm@11.21.0 + + - name: Install Telescope dependencies + working-directory: src/telescope + run: npm ci + + - name: Run Telescope dashboard tests + working-directory: src/telescope + run: npm test diff --git a/src/ai/README.md b/src/ai/README.md index 556552288e..4db416d956 100644 --- a/src/ai/README.md +++ b/src/ai/README.md @@ -27,7 +27,7 @@ Skill discovery is cached for the fixed paths in `ai.skills.cached_paths`. [Relo The conversation migrations use Hypervel's column types, indexes, and approval claim fields. Existing Laravel conversation data requires a [schema migration](https://hypervel.org/docs/ai-sdk#migrating-conversation-data) before use. -Remote downloads share the HTTP client's destination checks. The protected `UntrustedUrl::validate()`, `resolve()`, and `isBlocked()` helpers and hostname-blocklist constants are removed; `resolveUsing()` remains available. URLs containing embedded credentials are rejected. Downloads are limited to 32 MiB of decoded content by default; configure `ai.remote_files.max_size` to change or disable the limit. See [remote attachments](https://hypervel.org/docs/ai-sdk#remote-attachments). +Remote downloads share the HTTP client's destination checks. The protected `UntrustedUrl::validate()`, `resolve()`, and `isBlocked()` helpers and hostname-blocklist constants are removed; `resolveUsing()` remains available. URLs containing embedded credentials are rejected. Downloads are limited to 32 MiB of decoded content by default, instead of Laravel's 100 MB; set `ai.remote_files.max_size` in kilobytes to change the limit, or to `null` to disable it. See [remote attachments](https://hypervel.org/docs/ai-sdk#remote-attachments). ## Known Issues diff --git a/src/ai/config/ai.php b/src/ai/config/ai.php index 07369e2a1b..9eff5d2426 100644 --- a/src/ai/config/ai.php +++ b/src/ai/config/ai.php @@ -89,13 +89,13 @@ | | Remote attachments must resolve to public addresses unless their host | is listed below. The size limit applies to each decoded download in - | bytes. Set max_size to null to allow downloads of any size. + | kilobytes. Set max_size to null to allow downloads of any size. | */ 'remote_files' => [ 'allowed_hosts' => [], - 'max_size' => 32 * 1024 * 1024, + 'max_size' => 32 * 1024, ], /* diff --git a/src/ai/src/Files/UntrustedUrl.php b/src/ai/src/Files/UntrustedUrl.php index 9f755e3b63..0a5c9d6900 100644 --- a/src/ai/src/Files/UntrustedUrl.php +++ b/src/ai/src/Files/UntrustedUrl.php @@ -14,15 +14,17 @@ use Hypervel\Http\Client\Response; use Hypervel\Support\Facades\Http; use InvalidArgumentException; +use Psr\Http\Message\ResponseInterface; +use RuntimeException; use Swoole\Coroutine\CanceledException; use Throwable; class UntrustedUrl { - public const int DEFAULT_MAX_SIZE = 32 * 1024 * 1024; - protected const int MAX_REDIRECTS = 5; + public const int MAX_KILOBYTES = 32 * 1024; + // REMOVED: hostname blocklists; the HTTP destination policy checks the resolved addresses. protected static ?Closure $resolver = null; @@ -36,16 +38,18 @@ public static function fetch(string $url): Response { $allowedHosts = config()->array('ai.remote_files.allowed_hosts', []); $policy = new RemoteFileDestinationPolicy(static::$resolver); - $maxSize = config()->get('ai.remote_files.max_size', static::DEFAULT_MAX_SIZE); + $kilobytes = config()->get('ai.remote_files.max_size', static::MAX_KILOBYTES); - if ($maxSize !== null && (! is_int($maxSize) || $maxSize <= 0)) { + if ($kilobytes !== null && (! is_int($kilobytes) || $kilobytes <= 0)) { throw new InvalidArgumentException('The remote file maximum size must be a positive integer or null.'); } + $limit = $kilobytes === null ? null : $kilobytes * 1024; + for ($hop = 0; $hop <= static::MAX_REDIRECTS; ++$hop) { $uri = new Uri($url); $options = []; - $tooLarge = null; + $tooLarge = false; $trusted = in_array(rtrim(trim($uri->getHost(), '[]'), '.'), $allowedHosts, true); if ($trusted) { @@ -54,20 +58,30 @@ public static function fetch(string $url): Response } } - if ($maxSize !== null) { - $buffer = Utils::streamFor(); + if ($limit !== null) { + $body = Utils::streamFor(); + + // The transport drops a compressed response's Content-Length before this runs, so only decoded lengths are checked. + $options['on_headers'] = static function (ResponseInterface $response) use ($limit, &$tooLarge): void { + if ((int) $response->getHeaderLine('Content-Length') > $limit) { + $tooLarge = true; - // Count decoded bytes at the sink; transfer sizes may describe compressed data. - $options['sink'] = FnStream::decorate($buffer, [ - 'write' => static function (string $chunk) use ($buffer, $maxSize, &$tooLarge): int { - if (strlen($chunk) > $maxSize - $buffer->tell()) { - $tooLarge = new InvalidArgumentException("The remote file exceeds the maximum size of [{$maxSize}] bytes."); + throw new RuntimeException('The remote file declares a body larger than the size limit.'); + } + }; + + // Count decoded bytes at the sink, which also bounds compressed and chunked bodies that declare no decoded length. + $options['sink'] = FnStream::decorate($body, [ + 'write' => static function (string $chunk) use ($body, $limit, &$tooLarge): int { + // The sink appends sequentially, so tell() gives its size without a stat call for every chunk. + if (strlen($chunk) > $limit - $body->tell()) { + $tooLarge = true; // A short write aborts the transfer while preserving transport cleanup. return 0; } - return $buffer->write($chunk); + return $body->write($chunk); }, ]); } @@ -87,7 +101,11 @@ public static function fetch(string $url): Response try { $response = $request->get((string) $uri); } catch (Throwable $exception) { - throw $exception instanceof CanceledException ? $exception : $tooLarge ?? $exception; + if ($tooLarge && ! $exception instanceof CanceledException) { + throw new InvalidArgumentException("The remote file exceeds the maximum size of {$kilobytes} kilobytes."); + } + + throw $exception; } if (! $response->redirect() || blank($response->header('Location'))) { diff --git a/src/ai/src/Gateway/Gemini/Concerns/MapsMessages.php b/src/ai/src/Gateway/Gemini/Concerns/MapsMessages.php index ca51d3308c..076e365a36 100644 --- a/src/ai/src/Gateway/Gemini/Concerns/MapsMessages.php +++ b/src/ai/src/Gateway/Gemini/Concerns/MapsMessages.php @@ -67,6 +67,11 @@ protected function mapAssistantMessage(AssistantMessage|Message $message, array return; } + // A signature never rides on a function call, so a persisted turn opens with the rebuilt thought step that held it... + if ($message instanceof AssistantMessage && filled($signature = $message->toolCalls->first()?->thoughtSignature)) { + $input[] = ['type' => 'thought', 'signature' => $signature]; + } + if (filled($message->content)) { $input[] = [ 'type' => 'model_output', @@ -75,11 +80,6 @@ protected function mapAssistantMessage(AssistantMessage|Message $message, array } if ($message instanceof AssistantMessage) { - // A signature never rides on a function call, so a persisted turn rebuilds the thought step that held it... - if (filled($signature = $message->toolCalls->first()?->thoughtSignature)) { - $input[] = ['type' => 'thought', 'signature' => $signature]; - } - foreach ($message->toolCalls as $toolCall) { $input[] = [ 'type' => 'function_call', diff --git a/src/docs/ai-sdk.md b/src/docs/ai-sdk.md index b74f102654..d3acc9f82a 100644 --- a/src/docs/ai-sdk.md +++ b/src/docs/ai-sdk.md @@ -1063,16 +1063,16 @@ Downloads inherit your application's outgoing proxy configuration; no separate A Pinning an untrusted hostname requires cURL with SSL support and libcurl 7.75 or later; see the HTTP client's [destination policy limitations](/docs/{{version}}/http-client#destination-policy-limitations). Explicitly trusted hosts bypass destination checks and do not require hostname pinning. -Downloads are limited to 32 MiB of decoded content by default. You may set a different positive byte limit, or `null` to disable the limit, in your `config/ai.php` file: +Downloads are limited to 32 MiB of decoded content by default. You may set a different limit in kilobytes, or `null` to disable the limit, in your `config/ai.php` file: ```php 'remote_files' => [ 'allowed_hosts' => ['files.internal.example'], - 'max_size' => 64 * 1024 * 1024, + 'max_size' => 64 * 1024, ], ``` -The limit also applies to allowed hosts and compressed responses. A blocked destination or oversized download throws an `InvalidArgumentException`. The limit applies to each file; concurrent downloads and preparing files for a provider can use additional memory. +The limit also applies to allowed hosts and compressed responses. A response that declares a larger `Content-Length` is rejected before its body is downloaded. A blocked destination or oversized download throws an `InvalidArgumentException`. The limit applies to each file; concurrent downloads and preparing files for a provider can use additional memory. If a file requires HTTP authentication, download it using the HTTP client's authentication methods and pass the downloaded content or local path to the SDK. The attachment's `withHeaders` method configures the upload to the AI provider, not the remote download. @@ -1642,7 +1642,7 @@ When using Anthropic, additional provider-specific options may be passed to the ### File Storage Tools -The `FileStorage` tool factory allows you to give agents access to a Hypervel [filesystem disk](/docs/{{version}}/filesystem). The `all` method returns tools that allow the agent to list, read, inspect, generate URLs for, write, delete, and copy files on the given disk: +The `FileStorage` tool factory allows you to give agents access to a Hypervel [filesystem disk](/docs/{{version}}/filesystem). The `all` method returns tools that allow the agent to list, read, inspect, generate URLs for, write, delete, copy, and move files on the given disk: ```php use Hypervel\Ai\Tools\FileStorage; @@ -1826,6 +1826,9 @@ To refine search results based on user location, use the `location` method: ); ``` +> [!NOTE] +> Groq only supports web search on its GPT-OSS models, and ignores the `max`, `allow`, and `location` methods. + #### Web Fetch @@ -1921,6 +1924,9 @@ When using OpenAI or Azure, you may make [stored files](#files) available to the ]); ``` +> [!NOTE] +> Groq only supports code execution on its GPT-OSS models. + ### Sub-Agents diff --git a/src/docs/blade.md b/src/docs/blade.md index 22c7f8806a..a0032d07b5 100644 --- a/src/docs/blade.md +++ b/src/docs/blade.md @@ -1881,6 +1881,8 @@ You may push to a stack as many times as needed. To render the complete stack co ``` +The `@stack` directive renders the content that has been pushed at the moment it is reached. Content pushed afterwards, such as from a component rendered below the `@stack` directive in the same layout component, will not be included. If you need to push from such components, place the `@stack` directive after them, for example at the end of the `
` tag. + If you would like to prepend content onto the beginning of a stack, you should use the `@prepend` directive: ```blade diff --git a/src/docs/csrf.md b/src/docs/csrf.md index 43d405e4d0..2d0b4984b8 100644 --- a/src/docs/csrf.md +++ b/src/docs/csrf.md @@ -31,7 +31,7 @@ Without CSRF protection, a malicious website could create an HTML form that poin If the malicious website automatically submits the form when the page is loaded, the malicious user only needs to lure an unsuspecting user of your application to visit their website and their email address will be changed in your application. -To prevent this vulnerability, we need to inspect every incoming `POST`, `PUT`, `PATCH`, or `DELETE` request for a secret session value that the malicious application is unable to access. +To prevent this vulnerability, we need to inspect every incoming `POST`, `PUT`, `PATCH`, `DELETE`, or `QUERY` request for a secret session value that the malicious application is unable to access. ## Preventing CSRF Requests @@ -147,4 +147,4 @@ Hypervel stores the current CSRF token in an encrypted `XSRF-TOKEN` cookie that This cookie is primarily sent as a developer convenience since some JavaScript frameworks and libraries, like Angular and Axios, automatically place its value in the `X-XSRF-TOKEN` header on same-origin requests. > [!NOTE] -> For JavaScript requests, read the `XSRF-TOKEN` cookie and send its value in the `X-XSRF-TOKEN` header on same-origin `POST`, `PUT`, `PATCH`, and `DELETE` requests. +> For JavaScript requests, read the `XSRF-TOKEN` cookie and send its value in the `X-XSRF-TOKEN` header on same-origin `POST`, `PUT`, `PATCH`, `DELETE`, and `QUERY` requests. diff --git a/src/docs/http-tests.md b/src/docs/http-tests.md index d38db75d2f..a5f0448486 100644 --- a/src/docs/http-tests.md +++ b/src/docs/http-tests.md @@ -70,8 +70,6 @@ $response = $this->query('/search', ['filter' => 'active']); $response = $this->queryJson('/search', ['filter' => 'active']); ``` -Because these requests are simulated, they aren't affected by the [HTTP/1.1 limitation on `QUERY` routes](/docs/{{version}}/routing#available-router-methods) that applies in production. - Instead of returning a `Hypervel\Http\Response` instance, test request methods return an instance of `Hypervel\Testing\TestResponse`, which provides a [variety of helpful assertions](#available-assertions) that allow you to inspect your application's responses: ```php tab=Pest diff --git a/src/docs/mcp.md b/src/docs/mcp.md index e80206a069..a777474e9a 100644 --- a/src/docs/mcp.md +++ b/src/docs/mcp.md @@ -1810,6 +1810,8 @@ Icons defined via the attribute and the `icons` method are combined automaticall Just like routes, you can authenticate web MCP servers with middleware. Adding authentication to your MCP server will require a user to authenticate before using any capability of the server. +This section covers external AI clients accessing **your application's MCP servers**. To connect your application to another service's MCP server using a user's credentials, see [client authentication](#client-authentication) instead. + You may authenticate access to your MCP server using bearer tokens through [Sanctum](/docs/{{version}}/sanctum) or custom authentication middleware, or use OAuth through [Passport](/docs/{{version}}/passport). @@ -1969,6 +1971,8 @@ public function handle(Request $request): Response In addition to building servers, Hypervel MCP includes a client for connecting to other MCP servers, whether first-party or third-party. The client lets your application discover and call the tools exposed by an MCP server, which is especially useful for giving your [AI agents](/docs/{{version}}/ai-sdk#mcp-tools) access to capabilities provided by external MCP servers. +For example, a chat application may connect to GitHub's MCP server using each user's GitHub token, allowing its agent to answer questions about that user's repositories. In this scenario, your application is the MCP client; you do not need to create an MCP server or register `Mcp::web` routes. + ### Connecting to Servers @@ -2012,7 +2016,7 @@ HTTP clients support concurrent calls through the same client instance, with a s ### Named Clients -Instead of constructing a client each time you need it, you may register reusable, named clients. This is typically done in the `boot` method of a service provider using the `Mcp` facade: +Instead of constructing a client each time you need it, you may optionally register reusable, named clients. The `registerClient` method names an outgoing connection; it does not expose an MCP server or authorize a user. Registration is typically done in the `boot` method of a service provider using the `Mcp` facade: ```php use Hypervel\Mcp\Client; @@ -2036,6 +2040,9 @@ For queued work that needs OAuth, use a named client with OAuth configured in it ### Client Authentication + +#### Bearer Tokens + To connect to a web MCP server that is protected by a bearer token, use the `withToken` method. You may pass a token string or a closure that lazily resolves the token: ```php @@ -2051,16 +2058,26 @@ $client = Client::web('https://api.githubcopilot.com/mcp/')->withToken( ); ``` -For servers protected by [OAuth 2.1](#oauth), configure the client using the `withOAuth` method. This is the client-side counterpart to protecting your own servers with OAuth: +If you already obtained a suitable token through [Socialite](/docs/{{version}}/socialite) or another account-linking flow, pass that token to `withToken`; you do not need to register MCP OAuth routes. + + +#### Linking User Accounts With OAuth + +Imagine a chat application where users connect a project management service, then ask an agent questions such as "Which of my tasks are overdue?" The external project management service exposes an OAuth-protected MCP server. Your application must redirect each user to the service's consent screen, exchange their approval for a token, and store that token for future tool calls. + +Hypervel MCP handles this account-linking flow using `withOAuth` and `Mcp::oAuthRoutesFor`. First, register a named client in a service provider. The OAuth client credentials identify your application to the service, while `withToken` supplies the connected user's saved access token when calling tools: ```php use Hypervel\Mcp\Client; use Hypervel\Mcp\Facades\Mcp; +use Hypervel\Support\Facades\Auth; -Mcp::registerClient('github', fn () => Client::web('https://api.githubcopilot.com/mcp/')->withOAuth( - clientId: config('services.github_mcp.client_id'), - clientSecret: config('services.github_mcp.client_secret'), -)); +Mcp::registerClient('projects', fn () => Client::web('https://projects.example.com/mcp') + ->withOAuth( + clientId: config('services.projects.client_id'), + clientSecret: config('services.projects.client_secret'), + ) + ->withToken(fn () => Auth::user()->projects_token)); ``` > [!NOTE] @@ -2068,7 +2085,7 @@ Mcp::registerClient('github', fn () => Client::web('https://api.githubcopilot.co The authorization server must advertise support for the `S256` PKCE code challenge method in its authorization server metadata. Hypervel will reject the authorization attempt if PKCE support is not advertised. -Next, register the OAuth routes for the named client in your `routes/ai.php` file using the `oAuthRoutesFor` method. The closure you provide receives the client name and resulting `TokenSet` after the authorization code has been exchanged for an access token: +Next, register the account-linking routes in your `routes/ai.php` file. The callback receives the client name and resulting `TokenSet`, allowing you to save the token for the signed-in user: ```php use Hypervel\Http\RedirectResponse; @@ -2076,17 +2093,44 @@ use Hypervel\Mcp\Client\OAuth\TokenSet; use Hypervel\Mcp\Facades\Mcp; use Hypervel\Support\Facades\Auth; -Mcp::oAuthRoutesFor('github', function (string $client, TokenSet $token): RedirectResponse { +Mcp::oAuthRoutesFor('projects', function (string $client, TokenSet $token): RedirectResponse { Auth::user()->update([ - 'github_mcp_token' => $token->accessToken, - 'github_mcp_scope' => $token->scope, + 'projects_token' => $token->accessToken, + 'projects_scope' => $token->scope, ]); - return redirect('/dashboard'); -}); + return redirect('/chat'); +}, middleware: ['web', 'auth']); +``` + +The `oAuthRoutesFor` method registers three named routes: a connect route (`mcp.oauth.{client}.connect`) that redirects the user to the authorization server, a callback route (`mcp.oauth.{client}.callback`) that exchanges the authorization code and invokes your handler, and a public Client ID Metadata Document route (`mcp.oauth.{client}.client-metadata`). + +The connect and callback routes use the `web` middleware group by default; the account-linking example above adds `auth` to require a signed-in user. The metadata route does not use this middleware because the authorization server must be able to retrieve it. + +Your "Connect Project Service" button should send the user to the connect route: + +```php +return redirect()->route('mcp.oauth.projects.connect'); ``` -This registers three named routes: a connect route (`mcp.oauth.{client}.connect`) that redirects the user to the authorization server, a callback route (`mcp.oauth.{client}.callback`) that exchanges the authorization code and invokes your handler, and a public Client ID Metadata Document route (`mcp.oauth.{client}.client-metadata`). The connect and callback routes use the `web` middleware group by default, which you may override using the `middleware` argument. The metadata route does not use this middleware because the authorization server must be able to retrieve it. +Hypervel redirects the user to the service to approve access, exchanges the authorization code on their return, and invokes your callback. On subsequent chat requests, the named client uses the saved token. For example, an [AI SDK agent](/docs/{{version}}/ai-sdk#mcp-tools) may expose the service's tools: + +```php +public function tools(): iterable +{ + return [ + ...Mcp::client('projects')->tools(), + ]; +} +``` + +Store tokens using [encrypted casts](/docs/{{version}}/eloquent-mutators#encrypted-casting) and hide them from model serialization. If the service issues expiring tokens, also persist the refresh token and expiry information from the `TokenSet` and handle renewal before making tool calls. For queued agents, resolve credentials from an explicitly provided user instead of `Auth::user()`. + +> [!NOTE] +> `Mcp::oAuthRoutesFor` connects your users to an external service. In contrast, `Mcp::oauthRoutes` allows external AI clients to authenticate to your own MCP servers using Passport. You do not need Passport or `Mcp::oauthRoutes` for the account-linking flow above. + + +#### Client Metadata The metadata document describes your application as a public OAuth client and uses your application's `APP_URL` to generate the client ID and callback URL. Therefore, you should ensure the `APP_URL` environment variable is set correctly in production. You may customize the metadata route and provide additional metadata using the `clientMetadataUri` and `clientMetadata` arguments: @@ -2096,13 +2140,13 @@ use Hypervel\Mcp\Client\OAuth\TokenSet; use Hypervel\Mcp\Facades\Mcp; Mcp::oAuthRoutesFor( - 'github', + 'projects', function (string $client, TokenSet $token): RedirectResponse { // Store the token... return redirect('/dashboard'); }, - clientMetadataUri: 'oauth/github/client.json', + clientMetadataUri: 'oauth/projects/client.json', clientMetadata: [ 'client_name' => 'Acme Dashboard', 'logo_uri' => 'https://acme.com/logo.png', @@ -2112,12 +2156,6 @@ Mcp::oAuthRoutesFor( If you set a custom `redirectUri` through `withOAuth`, also include it in `clientMetadata['redirect_uris']` so the public metadata document advertises that callback. -To begin the authorization flow, redirect the user to the connect route: - -```php -return redirect()->route('mcp.oauth.github.connect'); -``` - #### OAuth Scopes and Authorization Challenges @@ -2137,10 +2175,10 @@ use Hypervel\Mcp\Facades\Mcp; use Hypervel\Support\Facades\Auth; try { - $tools = Mcp::client('github')->tools(); + $tools = Mcp::client('projects')->tools(); } catch (AuthorizationRequiredException $exception) { $query = $exception->query(); - $grantedScope = Auth::user()->github_mcp_scope; + $grantedScope = Auth::user()->projects_scope; if ($grantedScope !== null) { $query['scope'] = implode(' ', array_unique(explode(' ', trim( @@ -2148,7 +2186,7 @@ try { )))); } - return redirect()->signedRoute('mcp.oauth.github.connect', $query); + return redirect()->signedRoute('mcp.oauth.projects.connect', $query); } ``` diff --git a/src/docs/permission.md b/src/docs/permission.md index 19446906d8..cea7b443da 100644 --- a/src/docs/permission.md +++ b/src/docs/permission.md @@ -1808,6 +1808,8 @@ $user->syncPermissionEffects( Changing a model's roles or direct permissions clears only that model's cached assignments. Changing a role's permissions, or creating, updating, or deleting a role or permission, clears the cached catalog. Calling `syncModels`, hard deleting a role or permission, and resetting the cache expire every model's cached assignments. +Roles and permissions returned by `findByName`, `findById`, and the other lookups are shared within the current request or job. If you change one of these models, save it before looking up roles or permissions again. + If you change the permission tables any other way, such as with raw queries, reset the cache yourself using the `permission:cache-reset` command or the `forgetCachedPermissions` method: ```shell diff --git a/src/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md index 60fb92329f..6a6b6081bd 100644 --- a/src/docs/porting-from-laravel.md +++ b/src/docs/porting-from-laravel.md @@ -35,6 +35,7 @@ - [CSRF Protection](#csrf-protection) - [Fortify](#fortify) - [Passport](#passport) + - [Permission](#permission) - [JWT Authentication](#jwt-authentication) - [Scout](#scout) - [Sentry](#sentry) @@ -537,7 +538,7 @@ Agent broadcasts also batch adjacent text/reasoning deltas by default. Each batc Agent overrides of `Promptable::getProvidersAndModels()` must return an ordered list of provider/model pairs, such as `[['openai', 'gpt-4o']]`, instead of `['openai' => 'gpt-4o']`. The provider may also be a `Provider` object, preserving its credentials. Failover lists retain repeated entries as separate attempts. See [failover](/docs/{{version}}/ai-sdk#failover). -Remote attachments share the HTTP client's destination checks instead of `UntrustedUrl`'s protected `validate`, `resolve`, and `isBlocked` helpers and hostname-blocklist constants. Adapt subclasses that override them. URLs with embedded credentials are rejected; fetch authenticated files explicitly before attaching them. Configure `ai.remote_files.max_size` if your downloads exceed the default 32 MiB limit. See [remote attachments](/docs/{{version}}/ai-sdk#remote-attachments). +Remote attachments share the HTTP client's destination checks instead of `UntrustedUrl`'s protected `validate`, `resolve`, and `isBlocked` helpers and hostname-blocklist constants. Adapt subclasses that override them. URLs with embedded credentials are rejected; fetch authenticated files explicitly before attaching them. The default `ai.remote_files.max_size` is 32 MiB (`32 * 1024` kilobytes) instead of Laravel's 100 MB; raise it if your downloads are larger. See [remote attachments](/docs/{{version}}/ai-sdk#remote-attachments). Skill definitions in `resources/skills` are cached across requests. If your application changes them at runtime, use a closure or an uncached directory, or set `ai.skills.cached_paths` to `[]`. See [skills](/docs/{{version}}/ai-sdk#skills). @@ -628,6 +629,11 @@ Restart the server and other long-running processes after changing file or envir Custom guards, controllers, middleware and cookie-factory subclasses must match Hypervel's constructor and protected method signatures. In particular, guards read `RequestContext` instead of accepting `setRequest()`, `CreateFreshApiToken` passes its guard explicitly, and server-building methods live on `ServerFactory`. Consult the package's [API differences](https://github.com/hypervel/passport#differences-from-laravel) and [Passport documentation](/docs/{{version}}/passport) when porting extensions. + +### Permission + +Replace the cache configuration's `expiration_time` with `expiration_seconds`, and use a cache store that keeps values and atomic locks on the same backend. See [cache configuration](/docs/{{version}}/permission#cache). Roles and permissions returned by lookups are shared within the current request or job, so code that changes one must save it before looking up roles or permissions again. See [caching](/docs/{{version}}/permission#caching). + ### JWT Authentication diff --git a/src/docs/routing.md b/src/docs/routing.md index e1b1d19482..686c1ffd00 100644 --- a/src/docs/routing.md +++ b/src/docs/routing.md @@ -112,9 +112,9 @@ Route::options($uri, $callback); Route::query($uri, $callback); ``` -The `query` method registers a route for HTTP `QUERY` requests. Like `GET`, a `QUERY` request only reads data, but it sends its query in the request body, which suits searches that are too large or complex to fit in a URL. +The `query` method registers a route for HTTP `QUERY` requests. Like `GET`, a `QUERY` request only reads data, but it sends its query in the request body, which suits searches that are too large or complex to fit in a URL. `QUERY` routes in the `web` middleware group are subject to [CSRF protection](/docs/{{version}}/csrf). -Sometimes you may need to register a route that responds to multiple HTTP verbs. You may do so using the `match` method. Or, you may even register a route that responds to all HTTP verbs using the `any` method: +Sometimes you may need to register a route that responds to multiple HTTP verbs. You may do so using the `match` method. Or, you may even register a route that responds to all HTTP verbs, including `QUERY`, using the `any` method: ```php Route::match(['get', 'post'], '/', function () { diff --git a/src/docs/urls.md b/src/docs/urls.md index a39a459440..36ca0ce5aa 100644 --- a/src/docs/urls.md +++ b/src/docs/urls.md @@ -340,6 +340,9 @@ ValidateSignature::except(['page']); The `except` method stores the ignored parameters for the worker lifetime, so it should be called during application boot, not from request-specific code. +> [!WARNING] +> A signature only covers the URL, including its query string. Any other request input, such as a form or JSON request body, is not signed and can be freely modified by the user. Since Hypervel merges these input sources together and gives the request body precedence over the query string, retrieving input with any method other than `query`, such as `input`, `all`, or a dynamic request property, may return unsigned values that override your signed query string values. Therefore, you should always retrieve signed values using the `query` method, such as `$request->query('email')`. This includes `GET` requests, as a specially crafted `GET` request can still contain a JSON request body. + #### Responding to Invalid Signed Routes diff --git a/src/docs/wayfinder.md b/src/docs/wayfinder.md index 8c93475c97..0f4bb041b4 100644 --- a/src/docs/wayfinder.md +++ b/src/docs/wayfinder.md @@ -74,6 +74,12 @@ php artisan wayfinder:generate --skip-actions php artisan wayfinder:generate --skip-routes ``` +The `--skip-database` option skips the database schema when typing route model bindings and reads the model's `@property` docblocks instead. Use it when the database is unreachable at build time. Eloquent casts are still used, and bindings without a matching cast or `@property` tag fall back to `string | number | boolean`: + +```shell +php artisan wayfinder:generate --skip-database +``` + When the Vite plugin regenerates Wayfinder output during every build, the generated directories may be added to your application's `.gitignore` file. If your deployment does not install Composer dependencies or run the generator, commit the generated files instead and regenerate them whenever your routes or controllers change. @@ -108,6 +114,15 @@ show.url(1); // '/posts/1' show.head(1); // { url: '/posts/1', method: 'head' } ``` +Routes registered with the [`QUERY`](/docs/{{version}}/routing#available-router-methods) method, such as `Route::query('/search', ...)`, receive a `query` method like any other verb: + +```ts +import { search } from '@/actions/App/Http/Controllers/SearchController'; + +search(); // { url: '/search', method: 'query' } +search.query(); // { url: '/search', method: 'query' } +``` + When a controller method uses a JavaScript reserved word such as `delete` or `import`, Wayfinder appends `Method` to the generated export name. For example, `delete` becomes `deleteMethod`. @@ -258,6 +273,8 @@ For actions that support multiple HTTP methods, you may select a method on the f const UpdatePost = () => ; ``` +Since HTML forms only support `GET` and `POST`, `QUERY` routes are spoofed the same way as `PUT`, `PATCH`, and `DELETE`: `search.form()` produces `