addSeconds($data['expires_in'] ?? 300), handle: $handle, issuer: $issuer, scope: isset($data['scope']) ? explode(' ', $data['scope']) : [], authType: AuthType::OAuth, ); } // Legacy createSession format (app passwords have full access) // Parse expiry from JWT since createSession doesn't return expiresIn $expiresAt = self::parseJwtExpiry($data['accessJwt']) ?? now()->addHour(); return new self( accessJwt: $data['accessJwt'], refreshJwt: $data['refreshJwt'], did: $data['did'], expiresAt: $expiresAt, handle: $data['handle'] ?? $handle, issuer: $issuer, scope: ['atproto', 'transition:generic', 'transition:email'], authType: AuthType::Legacy, ); } /** * Parse the expiry timestamp from a JWT's payload. */ protected static function parseJwtExpiry(string $jwt): ?\DateTimeInterface { $parts = explode('.', $jwt); if (count($parts) !== 3) { return null; } $payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true); if (! isset($payload['exp'])) { return null; } return Carbon::createFromTimestamp($payload['exp']); } }