diff --git a/src/php/src/Api/Controllers/Api/EntitlementApiController.php b/src/php/src/Api/Controllers/Api/EntitlementApiController.php new file mode 100644 index 0000000..81fcfb4 --- /dev/null +++ b/src/php/src/Api/Controllers/Api/EntitlementApiController.php @@ -0,0 +1,101 @@ +resolveWorkspace($request); + + if (! $workspace instanceof Workspace) { + return $this->noWorkspaceResponse(); + } + + $apiKey = $request->attributes->get('api_key'); + $authType = $request->attributes->get('auth_type', 'session'); + $rateLimitProfile = $this->resolveRateLimitProfile($authType); + $activeApiKeys = ApiKey::query() + ->forWorkspace($workspace->id) + ->active() + ->count(); + + $usage = $this->usageService->getWorkspaceSummary($workspace->id); + + return response()->json([ + 'workspace_id' => $workspace->id, + 'workspace' => [ + 'id' => $workspace->id, + 'name' => $workspace->name ?? null, + ], + 'authentication' => [ + 'type' => $authType, + 'scopes' => $apiKey instanceof ApiKey ? $apiKey->scopes : null, + ], + 'limits' => [ + 'rate_limit' => $rateLimitProfile, + 'api_keys' => [ + 'active' => $activeApiKeys, + 'maximum' => (int) config('api.keys.max_per_workspace', 10), + 'remaining' => max(0, (int) config('api.keys.max_per_workspace', 10) - $activeApiKeys), + ], + 'webhooks' => [ + 'maximum' => (int) config('api.webhooks.max_per_workspace', 5), + ], + ], + 'usage' => $usage, + 'features' => [ + 'pixel' => true, + 'mcp' => true, + 'webhooks' => true, + 'usage_alerts' => (bool) config('api.alerts.enabled', true), + ], + ]); + } + + /** + * Resolve the rate limit profile for the current auth context. + */ + protected function resolveRateLimitProfile(string $authType): array + { + $rateLimits = (array) config('api.rate_limits', []); + $key = $authType === 'session' ? 'default' : 'authenticated'; + $profile = (array) ($rateLimits[$key] ?? []); + + return [ + 'name' => $key, + 'limit' => (int) ($profile['limit'] ?? 0), + 'window' => (int) ($profile['window'] ?? 60), + 'burst' => (float) ($profile['burst'] ?? 1.0), + ]; + } +} diff --git a/src/php/src/Api/Routes/api.php b/src/php/src/Api/Routes/api.php index 638744e..f130142 100644 --- a/src/php/src/Api/Routes/api.php +++ b/src/php/src/Api/Routes/api.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Core\Api\Controllers\Api\UnifiedPixelController; +use Core\Api\Controllers\Api\EntitlementApiController; use Core\Api\Controllers\McpApiController; use Core\Api\Middleware\PublicApiCors; use Core\Mcp\Middleware\McpApiKeyAuth; @@ -32,6 +33,18 @@ Route::middleware([PublicApiCors::class, 'api.rate']) ->name('track'); }); +// ───────────────────────────────────────────────────────────────────────────── +// Entitlements (authenticated) +// ───────────────────────────────────────────────────────────────────────────── + +Route::middleware(['auth.api', 'api.scope.enforce']) + ->prefix('entitlements') + ->name('api.entitlements.') + ->group(function () { + Route::get('/', [EntitlementApiController::class, 'show']) + ->name('show'); + }); + // ───────────────────────────────────────────────────────────────────────────── // MCP HTTP Bridge (API key auth) // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/php/src/Api/Tests/Feature/EntitlementsEndpointTest.php b/src/php/src/Api/Tests/Feature/EntitlementsEndpointTest.php new file mode 100644 index 0000000..2ce835d --- /dev/null +++ b/src/php/src/Api/Tests/Feature/EntitlementsEndpointTest.php @@ -0,0 +1,54 @@ +user = User::factory()->create(); + $this->workspace = Workspace::factory()->create(); + $this->workspace->users()->attach($this->user->id, [ + 'role' => 'owner', + 'is_default' => true, + ]); + + $result = ApiKey::generate( + $this->workspace->id, + $this->user->id, + 'Entitlements Key', + [ApiKey::SCOPE_READ] + ); + + $this->plainKey = $result['plain_key']; + $this->apiKey = $result['api_key']; +}); + +it('returns entitlement limits and usage for the current workspace', function () { + app(ApiUsageService::class)->record( + apiKeyId: $this->apiKey->id, + workspaceId: $this->workspace->id, + endpoint: '/api/entitlements', + method: 'GET', + statusCode: 200, + responseTimeMs: 42, + ipAddress: '127.0.0.1', + userAgent: 'Pest' + ); + + $response = $this->getJson('/api/entitlements', [ + 'Authorization' => "Bearer {$this->plainKey}", + ]); + + $response->assertOk(); + $response->assertJsonPath('workspace_id', $this->workspace->id); + $response->assertJsonPath('authentication.type', 'api_key'); + $response->assertJsonPath('limits.api_keys.maximum', config('api.keys.max_per_workspace')); + $response->assertJsonPath('limits.api_keys.active', 1); + $response->assertJsonPath('usage.totals.requests', 1); + $response->assertJsonPath('features.mcp', true); +});