Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

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

Implement Phase 3: Session Management

+240
+17
src/Events/TokenRefreshed.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Events; 4 + 5 + use Illuminate\Foundation\Events\Dispatchable; 6 + use Illuminate\Queue\SerializesModels; 7 + use SocialDept\AtpClient\Data\AccessToken; 8 + 9 + class TokenRefreshed 10 + { 11 + use Dispatchable, SerializesModels; 12 + 13 + public function __construct( 14 + public readonly string $identifier, 15 + public readonly AccessToken $token, 16 + ) {} 17 + }
+16
src/Events/TokenRefreshing.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Events; 4 + 5 + use Illuminate\Foundation\Events\Dispatchable; 6 + use Illuminate\Queue\SerializesModels; 7 + 8 + class TokenRefreshing 9 + { 10 + use Dispatchable, SerializesModels; 11 + 12 + public function __construct( 13 + public readonly string $identifier, 14 + public readonly string $refreshToken, 15 + ) {} 16 + }
+7
src/Exceptions/SessionExpiredException.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Exceptions; 4 + 5 + class SessionExpiredException extends \Exception 6 + { 7 + }
+60
src/Session/Session.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Session; 4 + 5 + use SocialDept\AtpClient\Data\Credentials; 6 + use SocialDept\AtpClient\Data\DPoPKey; 7 + 8 + class Session 9 + { 10 + public function __construct( 11 + protected Credentials $credentials, 12 + protected DPoPKey $dpopKey, 13 + protected string $pdsEndpoint, 14 + ) {} 15 + 16 + public function identifier(): string 17 + { 18 + return $this->credentials->identifier; 19 + } 20 + 21 + public function did(): string 22 + { 23 + return $this->credentials->did; 24 + } 25 + 26 + public function accessToken(): string 27 + { 28 + return $this->credentials->accessToken; 29 + } 30 + 31 + public function refreshToken(): string 32 + { 33 + return $this->credentials->refreshToken; 34 + } 35 + 36 + public function dpopKey(): DPoPKey 37 + { 38 + return $this->dpopKey; 39 + } 40 + 41 + public function pdsEndpoint(): string 42 + { 43 + return $this->pdsEndpoint; 44 + } 45 + 46 + public function isExpired(): bool 47 + { 48 + return $this->credentials->isExpired(); 49 + } 50 + 51 + public function expiresIn(): int 52 + { 53 + return $this->credentials->expiresIn(); 54 + } 55 + 56 + public function withCredentials(Credentials $credentials): self 57 + { 58 + return new self($credentials, $this->dpopKey, $this->pdsEndpoint); 59 + } 60 + }
+140
src/Session/SessionManager.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Session; 4 + 5 + use Illuminate\Http\Client\Factory as HttpClient; 6 + use SocialDept\AtpClient\Auth\DPoPKeyManager; 7 + use SocialDept\AtpClient\Auth\TokenRefresher; 8 + use SocialDept\AtpClient\Contracts\CredentialProvider; 9 + use SocialDept\AtpClient\Contracts\KeyStore; 10 + use SocialDept\AtpClient\Data\AccessToken; 11 + use SocialDept\AtpClient\Events\TokenRefreshed; 12 + use SocialDept\AtpClient\Events\TokenRefreshing; 13 + use SocialDept\AtpClient\Exceptions\AuthenticationException; 14 + use SocialDept\AtpClient\Exceptions\SessionExpiredException; 15 + use SocialDept\AtpResolver\Facades\Resolver; 16 + 17 + class SessionManager 18 + { 19 + protected array $sessions = []; 20 + 21 + public function __construct( 22 + protected CredentialProvider $credentials, 23 + protected TokenRefresher $refresher, 24 + protected DPoPKeyManager $dpopManager, 25 + protected KeyStore $keyStore, 26 + protected HttpClient $http, 27 + protected int $refreshThreshold = 300, // 5 minutes 28 + ) {} 29 + 30 + /** 31 + * Get or create session for identifier 32 + */ 33 + public function session(string $identifier): Session 34 + { 35 + if (! isset($this->sessions[$identifier])) { 36 + $this->sessions[$identifier] = $this->createSession($identifier); 37 + } 38 + 39 + return $this->sessions[$identifier]; 40 + } 41 + 42 + /** 43 + * Ensure session is valid, refresh if needed 44 + */ 45 + public function ensureValid(string $identifier): Session 46 + { 47 + $session = $this->session($identifier); 48 + 49 + // Check if token needs refresh 50 + if ($session->expiresIn() < $this->refreshThreshold) { 51 + $session = $this->refreshSession($session); 52 + } 53 + 54 + return $session; 55 + } 56 + 57 + /** 58 + * Create session from app password 59 + */ 60 + public function fromAppPassword( 61 + string $identifier, 62 + string $password 63 + ): Session { 64 + $pdsEndpoint = Resolver::resolvePds($identifier); 65 + 66 + $response = $this->http->post($pdsEndpoint.'/xrpc/com.atproto.server.createSession', [ 67 + 'identifier' => $identifier, 68 + 'password' => $password, 69 + ]); 70 + 71 + if ($response->failed()) { 72 + throw new AuthenticationException('Login failed'); 73 + } 74 + 75 + $token = AccessToken::fromResponse($response->json()); 76 + 77 + // Store credentials 78 + $this->credentials->storeCredentials($identifier, $token); 79 + 80 + return $this->createSession($identifier); 81 + } 82 + 83 + /** 84 + * Create session from credentials 85 + */ 86 + protected function createSession(string $identifier): Session 87 + { 88 + $creds = $this->credentials->getCredentials($identifier); 89 + 90 + if (! $creds) { 91 + throw new SessionExpiredException("No credentials found for {$identifier}"); 92 + } 93 + 94 + // Get or create DPoP key 95 + $sessionId = 'session_'.hash('sha256', $creds->did); 96 + $dpopKey = $this->keyStore->get($sessionId); 97 + 98 + if (! $dpopKey) { 99 + $dpopKey = $this->dpopManager->generateKey($sessionId); 100 + } 101 + 102 + // Resolve PDS endpoint 103 + $pdsEndpoint = Resolver::resolvePds($creds->did); 104 + 105 + return new Session($creds, $dpopKey, $pdsEndpoint); 106 + } 107 + 108 + /** 109 + * Refresh session tokens 110 + */ 111 + protected function refreshSession(Session $session): Session 112 + { 113 + // Fire event before refresh (allows developers to invalidate old token) 114 + event(new TokenRefreshing($session->identifier(), $session->refreshToken())); 115 + 116 + $newToken = $this->refresher->refresh( 117 + refreshToken: $session->refreshToken(), 118 + pdsEndpoint: $session->pdsEndpoint(), 119 + dpopKey: $session->dpopKey(), 120 + ); 121 + 122 + // Update credentials (CRITICAL: refresh tokens are single-use) 123 + $this->credentials->updateCredentials( 124 + $session->identifier(), 125 + $newToken 126 + ); 127 + 128 + // Fire event after successful refresh 129 + event(new TokenRefreshed($session->identifier(), $newToken)); 130 + 131 + // Update session 132 + $newCreds = $this->credentials->getCredentials($session->identifier()); 133 + $newSession = $session->withCredentials($newCreds); 134 + 135 + // Update cached session 136 + $this->sessions[$session->identifier()] = $newSession; 137 + 138 + return $newSession; 139 + } 140 + }