1import 'package:dio/dio.dart'; 2 3import 'did_helpers.dart'; 4import 'identity_resolver_error.dart'; 5 6/// Options for handle resolution. 7class ResolveHandleOptions { 8 /// Whether to bypass cache 9 final bool noCache; 10 11 /// Cancellation token for the request 12 final CancelToken? cancelToken; 13 14 const ResolveHandleOptions({this.noCache = false, this.cancelToken}); 15} 16 17/// Interface for resolving atProto handles to DIDs. 18abstract class HandleResolver { 19 /// Resolves an atProto handle to a DID. 20 /// 21 /// Returns null if the handle doesn't resolve to a DID (but no error occurred). 22 /// Throws [HandleResolverError] if an unexpected error occurs during resolution. 23 Future<String?> resolve(String handle, [ResolveHandleOptions? options]); 24} 25 26/// XRPC-based handle resolver that uses com.atproto.identity.resolveHandle. 27/// 28/// This resolver makes HTTP requests to an atProto XRPC service (typically 29/// a PDS or entryway service) to resolve handles. 30class XrpcHandleResolver implements HandleResolver { 31 /// The base URL of the XRPC service 32 final Uri serviceUrl; 33 34 /// HTTP client for making requests 35 final Dio dio; 36 37 XrpcHandleResolver(String serviceUrl, {Dio? dio}) 38 : serviceUrl = Uri.parse(serviceUrl), 39 dio = dio ?? Dio(); 40 41 @override 42 Future<String?> resolve( 43 String handle, [ 44 ResolveHandleOptions? options, 45 ]) async { 46 final url = serviceUrl.resolve('/xrpc/com.atproto.identity.resolveHandle'); 47 final uri = url.replace(queryParameters: {'handle': handle}); 48 49 try { 50 final response = await dio.getUri( 51 uri, 52 options: Options( 53 headers: {if (options?.noCache ?? false) 'Cache-Control': 'no-cache'}, 54 validateStatus: (status) { 55 // Allow 400 and 200 status codes 56 return status == 200 || status == 400; 57 }, 58 ), 59 cancelToken: options?.cancelToken, 60 ); 61 62 final data = response.data; 63 64 // Handle 400 Bad Request (expected for invalid/unresolvable handles) 65 if (response.statusCode == 400) { 66 if (data is Map<String, dynamic>) { 67 final error = data['error'] as String?; 68 final message = data['message'] as String?; 69 70 // Expected response for handle that doesn't exist 71 if (error == 'InvalidRequest' && 72 message == 'Unable to resolve handle') { 73 return null; 74 } 75 } 76 77 throw HandleResolverError( 78 'Invalid response from resolveHandle method: ${response.data}', 79 ); 80 } 81 82 // Handle successful response 83 if (response.statusCode == 200) { 84 if (data is! Map<String, dynamic>) { 85 throw HandleResolverError( 86 'Invalid response format from resolveHandle method', 87 ); 88 } 89 90 final did = data['did']; 91 if (did is! String) { 92 throw HandleResolverError( 93 'Missing or invalid DID in resolveHandle response', 94 ); 95 } 96 97 // Validate that it's a proper atProto DID 98 if (!isAtprotoDid(did)) { 99 throw HandleResolverError( 100 'Invalid DID returned from resolveHandle method: $did', 101 ); 102 } 103 104 return did; 105 } 106 107 throw HandleResolverError( 108 'Unexpected status code from resolveHandle method: ${response.statusCode}', 109 ); 110 } on DioException catch (e) { 111 if (e.type == DioExceptionType.cancel) { 112 throw HandleResolverError('Handle resolution was cancelled'); 113 } 114 115 throw HandleResolverError('Failed to resolve handle: ${e.message}', e); 116 } catch (e) { 117 if (e is HandleResolverError) rethrow; 118 119 throw HandleResolverError('Unexpected error resolving handle: $e', e); 120 } 121 } 122} 123 124/// Cached handle resolver that wraps another resolver with caching. 125class CachedHandleResolver implements HandleResolver { 126 final HandleResolver _resolver; 127 final HandleCache _cache; 128 129 CachedHandleResolver(this._resolver, [HandleCache? cache]) 130 : _cache = cache ?? InMemoryHandleCache(); 131 132 @override 133 Future<String?> resolve( 134 String handle, [ 135 ResolveHandleOptions? options, 136 ]) async { 137 // Check cache first unless noCache is set 138 if (!(options?.noCache ?? false)) { 139 final cached = await _cache.get(handle); 140 if (cached != null) { 141 return cached; 142 } 143 } 144 145 // Resolve and cache 146 final did = await _resolver.resolve(handle, options); 147 if (did != null) { 148 await _cache.set(handle, did); 149 } 150 151 return did; 152 } 153 154 /// Clears the cache 155 Future<void> clearCache() => _cache.clear(); 156} 157 158/// Interface for caching handle resolution results. 159abstract class HandleCache { 160 Future<String?> get(String handle); 161 Future<void> set(String handle, String did); 162 Future<void> clear(); 163} 164 165/// Simple in-memory handle cache with expiration. 166class InMemoryHandleCache implements HandleCache { 167 final Map<String, _CacheEntry> _cache = {}; 168 final Duration _ttl; 169 170 InMemoryHandleCache({Duration? ttl}) : _ttl = ttl ?? const Duration(hours: 1); 171 172 @override 173 Future<String?> get(String handle) async { 174 final entry = _cache[handle]; 175 if (entry == null) return null; 176 177 // Check if expired 178 if (DateTime.now().isAfter(entry.expiresAt)) { 179 _cache.remove(handle); 180 return null; 181 } 182 183 return entry.did; 184 } 185 186 @override 187 Future<void> set(String handle, String did) async { 188 _cache[handle] = _CacheEntry(did: did, expiresAt: DateTime.now().add(_ttl)); 189 } 190 191 @override 192 Future<void> clear() async { 193 _cache.clear(); 194 } 195} 196 197class _CacheEntry { 198 final String did; 199 final DateTime expiresAt; 200 201 _CacheEntry({required this.did, required this.expiresAt}); 202}