Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

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

Add repo scope matching with action parameter support

+106
+106
src/Auth/ScopeChecker.php
··· 90 90 return true; 91 91 } 92 92 93 + // Handle repo: scopes with action semantics 94 + if (str_starts_with($pattern, 'repo:')) { 95 + foreach ($granted as $scope) { 96 + if (str_starts_with($scope, 'repo:') && $this->matchesRepoScope($pattern, $scope)) { 97 + return true; 98 + } 99 + } 100 + } 101 + 93 102 // Check for wildcard matches 94 103 $patternRegex = $this->patternToRegex($pattern); 95 104 ··· 108 117 } 109 118 110 119 return false; 120 + } 121 + 122 + /** 123 + * Check if a required repo scope is satisfied by a granted repo scope. 124 + * 125 + * Per AT Protocol spec: "If not defined, all operations are allowed." 126 + * - repo:collection (no action) grants ALL actions 127 + * - repo:collection?action=create grants only create 128 + * - repo:* grants all collections with all actions 129 + */ 130 + protected function matchesRepoScope(string $required, string $granted): bool 131 + { 132 + $requiredParsed = $this->parseRepoScope($required); 133 + $grantedParsed = $this->parseRepoScope($granted); 134 + 135 + // Check collection match (with wildcard support) 136 + if (! $this->collectionsMatch($requiredParsed['collection'], $grantedParsed['collection'])) { 137 + return false; 138 + } 139 + 140 + // If granted has no actions, it grants ALL actions 141 + if (empty($grantedParsed['actions'])) { 142 + return true; 143 + } 144 + 145 + // If required has no actions, we need all actions granted 146 + if (empty($requiredParsed['actions'])) { 147 + // Required needs all actions, but granted is restricted 148 + return false; 149 + } 150 + 151 + // Check if all required actions are in granted actions 152 + return empty(array_diff($requiredParsed['actions'], $grantedParsed['actions'])); 153 + } 154 + 155 + /** 156 + * Parse a repo scope into collection and actions. 157 + * 158 + * Handles formats like: 159 + * - repo:app.bsky.feed.post 160 + * - repo:app.bsky.feed.post?action=create 161 + * - repo:app.bsky.feed.post?action=create&action=update&action=delete 162 + * - repo:* 163 + * - repo:*?action=delete 164 + * 165 + * @return array{collection: string, actions: array<string>} 166 + */ 167 + protected function parseRepoScope(string $scope): array 168 + { 169 + $parts = explode('?', $scope, 2); 170 + $collection = substr($parts[0], 5); // Remove 'repo:' 171 + 172 + $actions = []; 173 + if (isset($parts[1])) { 174 + // Parse action=create&action=update&action=delete format 175 + // PHP's parse_str doesn't handle repeated params well 176 + preg_match_all('/action=([^&]+)/', $parts[1], $matches); 177 + if (! empty($matches[1])) { 178 + $actions = array_map('urldecode', $matches[1]); 179 + } 180 + } 181 + 182 + return ['collection' => $collection, 'actions' => $actions]; 183 + } 184 + 185 + /** 186 + * Check if a required collection matches a granted collection. 187 + */ 188 + protected function collectionsMatch(string $required, string $granted): bool 189 + { 190 + if ($granted === '*') { 191 + return true; 192 + } 193 + 194 + return $required === $granted; 195 + } 196 + 197 + /** 198 + * Check if the session has repo access for a specific collection and action. 199 + */ 200 + public function checkRepoScope(Session $session, string $collection, string $action): bool 201 + { 202 + $required = "repo:{$collection}?action={$action}"; 203 + 204 + return $this->sessionHasScope($session, $required); 205 + } 206 + 207 + /** 208 + * Check repo scope and handle enforcement based on configuration. 209 + * 210 + * @throws MissingScopeException 211 + */ 212 + public function checkRepoScopeOrFail(Session $session, string $collection, string $action): void 213 + { 214 + $required = "repo:{$collection}?action={$action}"; 215 + 216 + $this->checkOrFail($session, [$required]); 111 217 } 112 218 113 219 /**