feat(api): add entitlements endpoint

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 06:47:28 +00:00
parent db1efd502c
commit 797c5f9571
3 changed files with 168 additions and 0 deletions

View file

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Core\Api\Controllers\Api;
use Core\Api\Concerns\HasApiResponses;
use Core\Api\Concerns\ResolvesWorkspace;
use Core\Api\Models\ApiKey;
use Core\Api\Services\ApiUsageService;
use Core\Front\Controller;
use Core\Tenant\Models\Workspace;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Entitlements API controller.
*
* Returns the current workspace's plan limits and usage snapshot.
*/
class EntitlementApiController extends Controller
{
use HasApiResponses;
use ResolvesWorkspace;
public function __construct(
protected ApiUsageService $usageService
) {
}
/**
* Show the current workspace entitlements.
*
* GET /api/entitlements
*/
public function show(Request $request): JsonResponse
{
$workspace = $this->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),
];
}
}

View file

@ -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)
// ─────────────────────────────────────────────────────────────────────────────

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
use Mod\Api\Models\ApiKey;
use Mod\Api\Services\ApiUsageService;
use Mod\Tenant\Models\User;
use Mod\Tenant\Models\Workspace;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
$this->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);
});