Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add ScopeChecker service for scope validation

+162
+162
src/Auth/ScopeChecker.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Auth; 4 + 5 + use Illuminate\Support\Facades\Log; 6 + use SocialDept\AtpClient\Enums\Scope; 7 + use SocialDept\AtpClient\Enums\ScopeEnforcementLevel; 8 + use SocialDept\AtpClient\Exceptions\MissingScopeException; 9 + use SocialDept\AtpClient\Session\Session; 10 + 11 + class ScopeChecker 12 + { 13 + public function __construct( 14 + protected ScopeEnforcementLevel $enforcement = ScopeEnforcementLevel::Permissive 15 + ) {} 16 + 17 + /** 18 + * Check if the session has all required scopes. 19 + * 20 + * @param array<string|Scope> $requiredScopes 21 + */ 22 + public function check(Session $session, array $requiredScopes): bool 23 + { 24 + $required = $this->normalizeScopes($requiredScopes); 25 + $granted = $session->scopes(); 26 + 27 + foreach ($required as $scope) { 28 + if (! $this->sessionHasScope($session, $scope)) { 29 + return false; 30 + } 31 + } 32 + 33 + return true; 34 + } 35 + 36 + /** 37 + * Check scopes and handle enforcement based on configuration. 38 + * 39 + * @param array<string|Scope> $requiredScopes 40 + * 41 + * @throws MissingScopeException 42 + */ 43 + public function checkOrFail(Session $session, array $requiredScopes): void 44 + { 45 + if ($this->check($session, $requiredScopes)) { 46 + return; 47 + } 48 + 49 + $required = $this->normalizeScopes($requiredScopes); 50 + $granted = $session->scopes(); 51 + $missing = array_diff($required, $granted); 52 + 53 + if ($this->enforcement === ScopeEnforcementLevel::Strict) { 54 + throw new MissingScopeException($missing, $granted); 55 + } 56 + 57 + Log::warning('ATP Client: Missing required scope(s)', [ 58 + 'required' => $required, 59 + 'granted' => $granted, 60 + 'missing' => $missing, 61 + 'did' => $session->did(), 62 + ]); 63 + } 64 + 65 + /** 66 + * Check if the session has a specific scope. 67 + */ 68 + public function hasScope(Session $session, string|Scope $scope): bool 69 + { 70 + $scope = $scope instanceof Scope ? $scope->value : $scope; 71 + 72 + return $this->sessionHasScope($session, $scope); 73 + } 74 + 75 + /** 76 + * Check if the session matches a granular scope pattern. 77 + * 78 + * Supports patterns like: 79 + * - repo:app.bsky.feed.post?action=create 80 + * - repo:app.bsky.feed.* 81 + * - rpc:app.bsky.feed.* 82 + * - blob:image/* 83 + */ 84 + public function matchesGranular(Session $session, string $pattern): bool 85 + { 86 + $granted = $session->scopes(); 87 + 88 + // Check for exact match first 89 + if (in_array($pattern, $granted, true)) { 90 + return true; 91 + } 92 + 93 + // Check for wildcard matches 94 + $patternRegex = $this->patternToRegex($pattern); 95 + 96 + foreach ($granted as $scope) { 97 + if (preg_match($patternRegex, $scope)) { 98 + return true; 99 + } 100 + } 101 + 102 + // Check if granted scope is a superset (wildcard in granted scope) 103 + foreach ($granted as $scope) { 104 + $grantedRegex = $this->patternToRegex($scope); 105 + if (preg_match($grantedRegex, $pattern)) { 106 + return true; 107 + } 108 + } 109 + 110 + return false; 111 + } 112 + 113 + /** 114 + * Get the current enforcement level. 115 + */ 116 + public function enforcement(): ScopeEnforcementLevel 117 + { 118 + return $this->enforcement; 119 + } 120 + 121 + /** 122 + * Create a new instance with a different enforcement level. 123 + */ 124 + public function withEnforcement(ScopeEnforcementLevel $enforcement): self 125 + { 126 + return new self($enforcement); 127 + } 128 + 129 + /** 130 + * @param array<string|Scope> $scopes 131 + * @return array<string> 132 + */ 133 + protected function normalizeScopes(array $scopes): array 134 + { 135 + return array_map( 136 + fn ($scope) => $scope instanceof Scope ? $scope->value : $scope, 137 + $scopes 138 + ); 139 + } 140 + 141 + protected function sessionHasScope(Session $session, string $scope): bool 142 + { 143 + // Direct match 144 + if ($session->hasScope($scope)) { 145 + return true; 146 + } 147 + 148 + // Check granular pattern matching 149 + return $this->matchesGranular($session, $scope); 150 + } 151 + 152 + protected function patternToRegex(string $pattern): string 153 + { 154 + // Escape regex special characters except * 155 + $escaped = preg_quote($pattern, '/'); 156 + 157 + // Replace \* with .* for wildcard matching 158 + $regex = str_replace('\*', '.*', $escaped); 159 + 160 + return '/^'.$regex.'$/'; 161 + } 162 + }