'array', 'rate_limit' => 'integer', 'call_count' => 'integer', 'last_used_at' => 'datetime', 'expires_at' => 'datetime', 'revoked_at' => 'datetime', 'ip_restriction_enabled' => 'boolean', 'ip_whitelist' => 'array', ]; protected $hidden = [ 'key', ]; /** * The plaintext key (only available after creation). */ public ?string $plainTextKey = null; // Permission constants public const PERM_PLANS_READ = 'plans.read'; public const PERM_PLANS_WRITE = 'plans.write'; public const PERM_PHASES_WRITE = 'phases.write'; public const PERM_SESSIONS_READ = 'sessions.read'; public const PERM_SESSIONS_WRITE = 'sessions.write'; public const PERM_TOOLS_READ = 'tools.read'; public const PERM_TEMPLATES_READ = 'templates.read'; public const PERM_TEMPLATES_INSTANTIATE = 'templates.instantiate'; // Notify module permissions public const PERM_NOTIFY_READ = 'notify:read'; public const PERM_NOTIFY_WRITE = 'notify:write'; public const PERM_NOTIFY_SEND = 'notify:send'; /** * All available permissions with descriptions. */ public static function availablePermissions(): array { return [ self::PERM_PLANS_READ => 'List and view plans', self::PERM_PLANS_WRITE => 'Create, update, archive plans', self::PERM_PHASES_WRITE => 'Update phase status, add/complete tasks', self::PERM_SESSIONS_READ => 'List and view sessions', self::PERM_SESSIONS_WRITE => 'Start, update, complete sessions', self::PERM_TOOLS_READ => 'View tool analytics', self::PERM_TEMPLATES_READ => 'List and view templates', self::PERM_TEMPLATES_INSTANTIATE => 'Create plans from templates', // Notify module self::PERM_NOTIFY_READ => 'List and view push campaigns, subscribers, and websites', self::PERM_NOTIFY_WRITE => 'Create, update, and delete campaigns and subscribers', self::PERM_NOTIFY_SEND => 'Send push notifications immediately or schedule sends', ]; } // Relationships public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } // Scopes public function scopeActive($query) { return $query->whereNull('revoked_at') ->where(function ($q) { $q->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }); } public function scopeForWorkspace($query, Workspace|int $workspace) { $workspaceId = $workspace instanceof Workspace ? $workspace->id : $workspace; return $query->where('workspace_id', $workspaceId); } public function scopeRevoked($query) { return $query->whereNotNull('revoked_at'); } public function scopeExpired($query) { return $query->whereNotNull('expires_at') ->where('expires_at', '<=', now()); } // Factory public static function generate( Workspace|int $workspace, string $name, array $permissions = [], int $rateLimit = 100, ?\Carbon\Carbon $expiresAt = null ): self { $workspaceId = $workspace instanceof Workspace ? $workspace->id : $workspace; // Generate a random key $plainKey = 'ak_'.Str::random(32); $key = static::create([ 'workspace_id' => $workspaceId, 'name' => $name, 'key' => hash('sha256', $plainKey), 'permissions' => $permissions, 'rate_limit' => $rateLimit, 'call_count' => 0, 'expires_at' => $expiresAt, ]); // Store plaintext key for one-time display $key->plainTextKey = $plainKey; return $key; } /** * Find a key by its plaintext value. */ public static function findByKey(string $plainKey): ?self { $hash = hash('sha256', $plainKey); return static::where('key', $hash)->first(); } // Status helpers public function isActive(): bool { if ($this->revoked_at !== null) { return false; } if ($this->expires_at !== null && $this->expires_at->isPast()) { return false; } return true; } public function isRevoked(): bool { return $this->revoked_at !== null; } public function isExpired(): bool { return $this->expires_at !== null && $this->expires_at->isPast(); } // Permission helpers public function hasPermission(string $permission): bool { return in_array($permission, $this->permissions ?? []); } public function hasAnyPermission(array $permissions): bool { foreach ($permissions as $permission) { if ($this->hasPermission($permission)) { return true; } } return false; } public function hasAllPermissions(array $permissions): bool { foreach ($permissions as $permission) { if (! $this->hasPermission($permission)) { return false; } } return true; } // Actions public function revoke(): self { $this->update(['revoked_at' => now()]); return $this; } public function recordUsage(): self { $this->increment('call_count'); $this->update(['last_used_at' => now()]); return $this; } public function updatePermissions(array $permissions): self { $this->update(['permissions' => $permissions]); return $this; } public function updateRateLimit(int $rateLimit): self { $this->update(['rate_limit' => $rateLimit]); return $this; } public function extendExpiry(\Carbon\Carbon $expiresAt): self { $this->update(['expires_at' => $expiresAt]); return $this; } public function removeExpiry(): self { $this->update(['expires_at' => null]); return $this; } // IP Restriction helpers /** * Enable IP restrictions for this key. */ public function enableIpRestriction(): self { $this->update(['ip_restriction_enabled' => true]); return $this; } /** * Disable IP restrictions for this key. */ public function disableIpRestriction(): self { $this->update(['ip_restriction_enabled' => false]); return $this; } /** * Update the IP whitelist. * * @param array $whitelist */ public function updateIpWhitelist(array $whitelist): self { $this->update(['ip_whitelist' => $whitelist]); return $this; } /** * Add an IP or CIDR to the whitelist. */ public function addToIpWhitelist(string $ipOrCidr): self { $whitelist = $this->ip_whitelist ?? []; if (! in_array($ipOrCidr, $whitelist, true)) { $whitelist[] = $ipOrCidr; $this->update(['ip_whitelist' => $whitelist]); } return $this; } /** * Remove an IP or CIDR from the whitelist. */ public function removeFromIpWhitelist(string $ipOrCidr): self { $whitelist = $this->ip_whitelist ?? []; $whitelist = array_values(array_filter($whitelist, fn ($entry) => $entry !== $ipOrCidr)); $this->update(['ip_whitelist' => $whitelist]); return $this; } /** * Record the last used IP address. */ public function recordLastUsedIp(string $ip): self { $this->update(['last_used_ip' => $ip]); return $this; } /** * Check if IP restrictions are enabled and configured. */ public function hasIpRestrictions(): bool { return $this->ip_restriction_enabled && ! empty($this->ip_whitelist); } /** * Get the count of whitelisted entries. */ public function getIpWhitelistCount(): int { return count($this->ip_whitelist ?? []); } // Rate limiting public function isRateLimited(): bool { // Check calls in the last minute $recentCalls = $this->getRecentCallCount(); return $recentCalls >= $this->rate_limit; } public function getRecentCallCount(int $seconds = 60): int { // Use Laravel's cache to track calls per minute // The AgentApiKeyService increments this key on each authenticated request $cacheKey = "agent_api_key_rate:{$this->id}"; return (int) \Illuminate\Support\Facades\Cache::get($cacheKey, 0); } public function getRemainingCalls(): int { return max(0, $this->rate_limit - $this->getRecentCallCount()); } // Display helpers public function getMaskedKey(): string { // Show first 6 chars of the hashed key (not the plaintext) return 'ak_'.substr($this->key, 0, 6).'...'; } public function getStatusLabel(): string { if ($this->isRevoked()) { return 'Revoked'; } if ($this->isExpired()) { return 'Expired'; } return 'Active'; } public function getStatusColor(): string { if ($this->isRevoked()) { return 'red'; } if ($this->isExpired()) { return 'amber'; } return 'green'; } public function getLastUsedForHumans(): string { if (! $this->last_used_at) { return 'Never'; } return $this->last_used_at->diffForHumans(); } public function getExpiresForHumans(): string { if (! $this->expires_at) { return 'Never'; } if ($this->isExpired()) { return 'Expired '.$this->expires_at->diffForHumans(); } return 'Expires '.$this->expires_at->diffForHumans(); } // Output public function toArray(): array { return [ 'id' => $this->id, 'workspace_id' => $this->workspace_id, 'name' => $this->name, 'permissions' => $this->permissions, 'rate_limit' => $this->rate_limit, 'call_count' => $this->call_count, 'last_used_at' => $this->last_used_at?->toIso8601String(), 'expires_at' => $this->expires_at?->toIso8601String(), 'revoked_at' => $this->revoked_at?->toIso8601String(), 'status' => $this->getStatusLabel(), 'ip_restriction_enabled' => $this->ip_restriction_enabled, 'ip_whitelist_count' => $this->getIpWhitelistCount(), 'last_used_ip' => $this->last_used_ip, 'created_at' => $this->created_at?->toIso8601String(), ]; } }