Laravel AT Protocol Client (alpha & unstable)
at dev 5.0 kB view raw
1<?php 2 3namespace SocialDept\AtpClient\Session; 4 5use Illuminate\Support\Facades\Http; 6use SocialDept\AtpClient\Auth\DPoPKeyManager; 7use SocialDept\AtpClient\Auth\TokenRefresher; 8use SocialDept\AtpClient\Contracts\CredentialProvider; 9use SocialDept\AtpClient\Contracts\KeyStore; 10use SocialDept\AtpClient\Data\AccessToken; 11use SocialDept\AtpClient\Events\SessionAuthenticated; 12use SocialDept\AtpClient\Events\SessionRefreshing; 13use SocialDept\AtpClient\Events\SessionUpdated; 14use SocialDept\AtpClient\Exceptions\AuthenticationException; 15use SocialDept\AtpClient\Exceptions\HandleResolutionException; 16use SocialDept\AtpClient\Exceptions\SessionExpiredException; 17use SocialDept\AtpResolver\Facades\Resolver; 18use SocialDept\AtpResolver\Support\Identity; 19 20class SessionManager 21{ 22 protected array $sessions = []; 23 24 public function __construct( 25 protected CredentialProvider $credentials, 26 protected TokenRefresher $refresher, 27 protected DPoPKeyManager $dpopManager, 28 protected KeyStore $keyStore, 29 protected int $refreshThreshold = 300, // 5 minutes 30 ) {} 31 32 /** 33 * Resolve an actor (handle or DID) to a DID. 34 * 35 * @throws HandleResolutionException 36 */ 37 protected function resolveToDid(string $actor): string 38 { 39 // If already a DID, return as-is 40 if (Identity::isDid($actor)) { 41 return $actor; 42 } 43 44 // Resolve handle to DID 45 $did = Resolver::handleToDid($actor); 46 47 if (! $did) { 48 throw new HandleResolutionException($actor); 49 } 50 51 return $did; 52 } 53 54 /** 55 * Get or create session for an actor. 56 */ 57 public function session(string $actor): Session 58 { 59 $did = $this->resolveToDid($actor); 60 61 if (! isset($this->sessions[$did])) { 62 $this->sessions[$did] = $this->createSession($did); 63 } 64 65 return $this->sessions[$did]; 66 } 67 68 /** 69 * Ensure session is valid, refresh if needed. 70 */ 71 public function ensureValid(string $actor): Session 72 { 73 $session = $this->session($actor); 74 75 // Check if token needs refresh 76 if ($session->expiresIn() < $this->refreshThreshold) { 77 $session = $this->refreshSession($session); 78 } 79 80 return $session; 81 } 82 83 /** 84 * Create session from app password. 85 */ 86 public function fromAppPassword( 87 string $actor, 88 string $password 89 ): Session { 90 $did = $this->resolveToDid($actor); 91 $pdsEndpoint = Resolver::resolvePds($did); 92 93 $response = Http::post($pdsEndpoint.'/xrpc/com.atproto.server.createSession', [ 94 'identifier' => $actor, 95 'password' => $password, 96 ]); 97 98 if ($response->failed()) { 99 throw new AuthenticationException('Login failed'); 100 } 101 102 $token = AccessToken::fromResponse($response->json(), $actor, $pdsEndpoint); 103 104 // Store credentials using DID as key 105 $this->credentials->storeCredentials($did, $token); 106 107 event(new SessionAuthenticated($token)); 108 109 return $this->createSession($did); 110 } 111 112 /** 113 * Create session from credentials 114 */ 115 protected function createSession(string $did): Session 116 { 117 $creds = $this->credentials->getCredentials($did); 118 119 if (! $creds) { 120 throw new SessionExpiredException("No credentials found for {$did}"); 121 } 122 123 // Get or create DPoP key 124 $sessionId = 'session_'.hash('sha256', $creds->did); 125 $dpopKey = $this->keyStore->get($sessionId); 126 127 if (! $dpopKey) { 128 $dpopKey = $this->dpopManager->generateKey($sessionId); 129 } 130 131 // Use stored issuer if available, otherwise resolve PDS endpoint 132 $pdsEndpoint = $creds->issuer ?? Resolver::resolvePds($creds->did); 133 134 return new Session($creds, $dpopKey, $pdsEndpoint); 135 } 136 137 /** 138 * Refresh session tokens 139 */ 140 protected function refreshSession(Session $session): Session 141 { 142 $did = $session->did(); 143 144 // Fire event before refresh (allows developers to invalidate old token) 145 event(new SessionRefreshing($session)); 146 147 $newToken = $this->refresher->refresh( 148 refreshToken: $session->refreshToken(), 149 pdsEndpoint: $session->pdsEndpoint(), 150 dpopKey: $session->dpopKey(), 151 handle: $session->handle(), 152 authType: $session->authType(), 153 ); 154 155 // Update credentials (CRITICAL: refresh tokens are single-use) 156 $this->credentials->updateCredentials($did, $newToken); 157 158 // Fire event after successful refresh 159 event(new SessionUpdated($session, $newToken)); 160 161 // Update session 162 $newCreds = $this->credentials->getCredentials($did); 163 $newSession = $session->withCredentials($newCreds); 164 165 // Update cached session 166 $this->sessions[$did] = $newSession; 167 168 return $newSession; 169 } 170}