resolveToDid($actor); if (! isset($this->sessions[$did])) { $this->sessions[$did] = $this->createSession($did); } return $this->sessions[$did]; } /** * Ensure session is valid, refresh if needed. */ public function ensureValid(string $actor): Session { $session = $this->session($actor); // Check if token needs refresh if ($session->expiresIn() < $this->refreshThreshold) { $session = $this->refreshSession($session); } return $session; } /** * Create session from app password. */ public function fromAppPassword( string $actor, string $password ): Session { $did = $this->resolveToDid($actor); $pdsEndpoint = Resolver::resolvePds($did); $response = Http::post($pdsEndpoint.'/xrpc/com.atproto.server.createSession', [ 'identifier' => $actor, 'password' => $password, ]); if ($response->failed()) { throw new AuthenticationException('Login failed'); } $token = AccessToken::fromResponse($response->json(), $actor, $pdsEndpoint); // Store credentials using DID as key $this->credentials->storeCredentials($did, $token); event(new SessionAuthenticated($token)); return $this->createSession($did); } /** * Create session from credentials */ protected function createSession(string $did): Session { $creds = $this->credentials->getCredentials($did); if (! $creds) { throw new SessionExpiredException("No credentials found for {$did}"); } // Get or create DPoP key $sessionId = 'session_'.hash('sha256', $creds->did); $dpopKey = $this->keyStore->get($sessionId); if (! $dpopKey) { $dpopKey = $this->dpopManager->generateKey($sessionId); } // Use stored issuer if available, otherwise resolve PDS endpoint $pdsEndpoint = $creds->issuer ?? Resolver::resolvePds($creds->did); return new Session($creds, $dpopKey, $pdsEndpoint); } /** * Refresh session tokens */ protected function refreshSession(Session $session): Session { $did = $session->did(); // Fire event before refresh (allows developers to invalidate old token) event(new SessionRefreshing($session)); $newToken = $this->refresher->refresh( refreshToken: $session->refreshToken(), pdsEndpoint: $session->pdsEndpoint(), dpopKey: $session->dpopKey(), handle: $session->handle(), authType: $session->authType(), ); // Update credentials (CRITICAL: refresh tokens are single-use) $this->credentials->updateCredentials($did, $newToken); // Fire event after successful refresh event(new SessionUpdated($session, $newToken)); // Update session $newCreds = $this->credentials->getCredentials($did); $newSession = $session->withCredentials($newCreds); // Update cached session $this->sessions[$did] = $newSession; return $newSession; } }