Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/formats.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
paths:
- '**.php'
- 'composer.json'
pull_request:
branches: [ 3.x, 4.x ]

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"kevinrob/guzzle-cache-middleware": "^4.0",
"monolog/monolog": "^3.7",
"nesbot/carbon": "^3.0",
"seatplus/esi-schema": "^1.0"
"seatplus/esi-schema": "^1.1"
},
"require-dev": {
"ext-openssl": "*",
Expand Down
50 changes: 29 additions & 21 deletions src/EsiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\UriInterface;
use Seatplus\EsiClient\DataTransferObjects\EsiAuthentication;
use Seatplus\EsiClient\Exceptions\EsiScopeAccessDeniedException;
use Seatplus\EsiClient\Exceptions\InvalidAuthenticationException;
use Seatplus\EsiClient\Exceptions\RequestFailedException;
use Seatplus\EsiClient\Exceptions\UriDataMissingException;
use Seatplus\EsiClient\Fetcher\GuzzleFetcher;
use Seatplus\EsiClient\Log\LogInterface;
use Seatplus\EsiClient\Services\CheckAccess;
use Seatplus\EsiSchema\Contracts\EsiCursor;
use Seatplus\EsiSchema\Contracts\EsiRawResponse;
use Seatplus\EsiSchema\Contracts\EsiTransportInterface;
use Seatplus\EsiSchema\Contracts\ScopeAccessDeniedException;
use Seatplus\EsiSchema\Resources\AllianceResource;
use Seatplus\EsiSchema\Resources\AssetsResource;
use Seatplus\EsiSchema\Resources\CalendarResource;
Expand Down Expand Up @@ -60,11 +59,9 @@ class EsiClient implements EsiTransportInterface
public function __construct(
private ?EsiAuthentication $authentication = null,
private ?GuzzleFetcher $fetcher = null,
private ?CheckAccess $checkAccess = null
) {
$this->fetcher ??= $this->createFetcher();
$this->logger = $this->createLogger();
$this->checkAccess ??= new CheckAccess($this->authentication);
}

/**
Expand All @@ -81,7 +78,6 @@ public function withToken(string $accessToken): static
refresh_token: '',
);
$clone->fetcher = $clone->createFetcher();
$clone->checkAccess = new CheckAccess($clone->authentication);

return $clone;
}
Expand Down Expand Up @@ -268,7 +264,7 @@ private function createFetcher(): GuzzleFetcher
* @throws \Throwable
* @throws UriDataMissingException
* @throws InvalidAuthenticationException
* @throws EsiScopeAccessDeniedException
* @throws ScopeAccessDeniedException
*/
public function invoke(
string $method,
Expand All @@ -279,15 +275,6 @@ public function invoke(
): EsiRawResponse {
// Enrich the uri
$uri = $this->buildDataUri($path, $pathValues, $queryParams);

// First check if access requirements are met
if (! $this->hasAccess($method, $path)) {
// Log the deny.
$this->logger->warning("Access denied to {$uri} due to missing scopes.");
throw new EsiScopeAccessDeniedException("Access denied to {$uri}");
}

// Fetcher will take care of caching
$response = $this->fetcher->call($method, $uri, $requestBody);

// Extract cursor tokens if the response body contains a `cursor` object.
Expand All @@ -312,6 +299,32 @@ public function invoke(
);
}

/**
* Assert that the current token possesses the required OAuth2 scope.
* Null = public endpoint — no-op.
*
* @throws ScopeAccessDeniedException
*/
/**
* Verify the authenticated token contains the required scope.
*
* Null means a public endpoint — always passes.
* Non-null throws ScopeAccessDeniedException if the scope is absent from the JWT.
*/
public function assertScope(?string $scope): void
{
if ($scope === null) {
return;
}

$scopes = $this->authentication?->getScopes() ?? [];

if (! in_array($scope, $scopes, true)) {
$this->logger->warning("Scope check failed: {$scope} not in token.");
throw new ScopeAccessDeniedException($scope);
}
}

private function createLogger(): LogInterface
{
return $this->getConfiguration()->getLogger();
Expand All @@ -332,7 +345,7 @@ private function buildDataUri(string $uri, array $data, array $query_parameters)
$query_params = array_merge(['datasource' => $this->getConfiguration('datasource')], $query_parameters);

$path = sprintf(
'/latest/%s/',
'/%s/',
trim($this->mapDataToUri($uri, $data), '/')
);

Expand Down Expand Up @@ -367,9 +380,4 @@ private function mapDataToUri(string $uri, array $data): string

return $uri;
}

private function hasAccess(string $method, string $uri_original): bool
{
return $this->checkAccess->can($method, $uri_original);
}
}
6 changes: 3 additions & 3 deletions src/EsiConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ public function __construct(
public string $fetcher = GuzzleFetcher::class,

// Versioning — X-Compatibility-Date header value (YYYY-MM-DD).
// Only needed for new-style ESI endpoints without a URL version prefix.
// Leave null for all existing versioned endpoints (/v5/...).
public ?string $compatibility_date = null,
// Sent on every request. Matches the ESI OpenAPI spec compatibility date
// used to generate seatplus/esi-schema. Update when regenerating the schema.
public ?string $compatibility_date = '2025-12-16',
) {}

public static function getInstance(...$args): self
Expand Down
5 changes: 0 additions & 5 deletions src/Exceptions/EsiScopeAccessDeniedException.php

This file was deleted.

Loading
Loading