1import 'package:dio/dio.dart'; 2 3import 'constants.dart'; 4import 'did_document.dart'; 5import 'did_helpers.dart'; 6import 'identity_resolver_error.dart'; 7 8/// Options for DID resolution. 9class ResolveDidOptions { 10 /// Whether to bypass cache 11 final bool noCache; 12 13 /// Cancellation token for the request 14 final CancelToken? cancelToken; 15 16 const ResolveDidOptions({this.noCache = false, this.cancelToken}); 17} 18 19/// Interface for resolving DIDs to DID documents. 20abstract class DidResolver { 21 /// Resolves a DID to its DID document. 22 /// 23 /// Throws [DidResolverError] if resolution fails. 24 Future<DidDocument> resolve(String did, [ResolveDidOptions? options]); 25} 26 27/// DID resolver that supports both did:plc and did:web methods. 28class AtprotoDidResolver implements DidResolver { 29 final DidPlcMethod _plcMethod; 30 final DidWebMethod _webMethod; 31 32 AtprotoDidResolver({String? plcDirectoryUrl, Dio? dio}) 33 : _plcMethod = DidPlcMethod(plcDirectoryUrl: plcDirectoryUrl, dio: dio), 34 _webMethod = DidWebMethod(dio: dio); 35 36 @override 37 Future<DidDocument> resolve(String did, [ResolveDidOptions? options]) async { 38 if (isDidPlc(did)) { 39 return _plcMethod.resolve(did, options); 40 } else if (isDidWeb(did)) { 41 return _webMethod.resolve(did, options); 42 } else { 43 throw DidResolverError( 44 'Unsupported DID method: ${extractDidMethod(did)}', 45 ); 46 } 47 } 48} 49 50/// Resolver for did:plc identifiers using the PLC directory. 51class DidPlcMethod { 52 final Uri plcDirectoryUrl; 53 final Dio dio; 54 55 DidPlcMethod({String? plcDirectoryUrl, Dio? dio}) 56 : plcDirectoryUrl = Uri.parse(plcDirectoryUrl ?? defaultPlcDirectoryUrl), 57 dio = dio ?? Dio(); 58 59 Future<DidDocument> resolve(String did, [ResolveDidOptions? options]) async { 60 assertDidPlc(did); 61 62 final url = plcDirectoryUrl.resolve('/${Uri.encodeComponent(did)}'); 63 64 try { 65 final response = await dio.getUri( 66 url, 67 options: Options( 68 headers: { 69 'Accept': 'application/did+ld+json,application/json', 70 if (options?.noCache ?? false) 'Cache-Control': 'no-cache', 71 }, 72 followRedirects: false, 73 validateStatus: (status) => status == 200, 74 ), 75 cancelToken: options?.cancelToken, 76 ); 77 78 if (response.data is! Map<String, dynamic>) { 79 throw DidResolverError( 80 'Invalid response format from PLC directory for $did', 81 ); 82 } 83 84 return DidDocument.fromJson(response.data as Map<String, dynamic>); 85 } on DioException catch (e) { 86 if (e.type == DioExceptionType.cancel) { 87 throw DidResolverError('DID resolution was cancelled'); 88 } 89 90 if (e.response?.statusCode == 404) { 91 throw DidResolverError('DID not found: $did'); 92 } 93 94 throw DidResolverError( 95 'Failed to resolve DID from PLC directory: ${e.message}', 96 e, 97 ); 98 } catch (e) { 99 if (e is DidResolverError) rethrow; 100 101 throw DidResolverError('Unexpected error resolving DID: $e', e); 102 } 103 } 104} 105 106/// Resolver for did:web identifiers using HTTPS. 107class DidWebMethod { 108 final Dio dio; 109 110 DidWebMethod({Dio? dio}) : dio = dio ?? Dio(); 111 112 Future<DidDocument> resolve(String did, [ResolveDidOptions? options]) async { 113 assertDidWeb(did); 114 115 final baseUrl = didWebToUrl(did); 116 117 // Try /.well-known/did.json first, then /did.json 118 final urls = [ 119 baseUrl.resolve('/.well-known/did.json'), 120 baseUrl.resolve('/did.json'), 121 ]; 122 123 DioException? lastError; 124 125 for (final url in urls) { 126 try { 127 final response = await dio.getUri( 128 url, 129 options: Options( 130 headers: { 131 'Accept': 'application/did+ld+json,application/json', 132 if (options?.noCache ?? false) 'Cache-Control': 'no-cache', 133 }, 134 followRedirects: false, 135 validateStatus: (status) => status == 200, 136 ), 137 cancelToken: options?.cancelToken, 138 ); 139 140 if (response.data is! Map<String, dynamic>) { 141 throw DidResolverError( 142 'Invalid response format from did:web for $did', 143 ); 144 } 145 146 final doc = DidDocument.fromJson(response.data as Map<String, dynamic>); 147 148 // Verify the DID in the document matches 149 if (doc.id != did) { 150 throw DidResolverError( 151 'DID mismatch: expected $did but got ${doc.id}', 152 ); 153 } 154 155 return doc; 156 } on DioException catch (e) { 157 if (e.type == DioExceptionType.cancel) { 158 throw DidResolverError('DID resolution was cancelled'); 159 } 160 161 // If not found, try the next URL 162 if (e.response?.statusCode == 404) { 163 lastError = e; 164 continue; 165 } 166 167 // Any other error, throw immediately 168 throw DidResolverError('Failed to resolve did:web: ${e.message}', e); 169 } catch (e) { 170 if (e is DidResolverError) rethrow; 171 172 throw DidResolverError('Unexpected error resolving did:web: $e', e); 173 } 174 } 175 176 // If we get here, all URLs failed 177 throw DidResolverError('DID document not found for $did', lastError); 178 } 179} 180 181/// Cached DID resolver that wraps another resolver with caching. 182class CachedDidResolver implements DidResolver { 183 final DidResolver _resolver; 184 final DidCache _cache; 185 186 CachedDidResolver(this._resolver, [DidCache? cache]) 187 : _cache = cache ?? InMemoryDidCache(); 188 189 @override 190 Future<DidDocument> resolve(String did, [ResolveDidOptions? options]) async { 191 // Check cache first unless noCache is set 192 if (!(options?.noCache ?? false)) { 193 final cached = await _cache.get(did); 194 if (cached != null) { 195 return cached; 196 } 197 } 198 199 // Resolve and cache 200 final doc = await _resolver.resolve(did, options); 201 await _cache.set(did, doc); 202 203 return doc; 204 } 205 206 /// Clears the cache 207 Future<void> clearCache() => _cache.clear(); 208} 209 210/// Interface for caching DID documents. 211abstract class DidCache { 212 Future<DidDocument?> get(String did); 213 Future<void> set(String did, DidDocument document); 214 Future<void> clear(); 215} 216 217/// Simple in-memory DID cache with expiration. 218class InMemoryDidCache implements DidCache { 219 final Map<String, _CacheEntry> _cache = {}; 220 final Duration _ttl; 221 222 InMemoryDidCache({Duration? ttl}) : _ttl = ttl ?? const Duration(hours: 24); 223 224 @override 225 Future<DidDocument?> get(String did) async { 226 final entry = _cache[did]; 227 if (entry == null) return null; 228 229 // Check if expired 230 if (DateTime.now().isAfter(entry.expiresAt)) { 231 _cache.remove(did); 232 return null; 233 } 234 235 return entry.document; 236 } 237 238 @override 239 Future<void> set(String did, DidDocument document) async { 240 _cache[did] = _CacheEntry( 241 document: document, 242 expiresAt: DateTime.now().add(_ttl), 243 ); 244 } 245 246 @override 247 Future<void> clear() async { 248 _cache.clear(); 249 } 250} 251 252class _CacheEntry { 253 final DidDocument document; 254 final DateTime expiresAt; 255 256 _CacheEntry({required this.document, required this.expiresAt}); 257}