1import 'package:dio/dio.dart'; 2 3import 'constants.dart'; 4import 'did_document.dart'; 5import 'did_helpers.dart'; 6import 'did_resolver.dart'; 7import 'handle_helpers.dart'; 8import 'handle_resolver.dart'; 9import 'identity_resolver_error.dart'; 10 11/// Represents resolved identity information for an atProto user. 12/// 13/// This combines DID, DID document, and validated handle information. 14class IdentityInfo { 15 /// The DID (Decentralized Identifier) for this identity 16 final String did; 17 18 /// The complete DID document 19 final DidDocument didDoc; 20 21 /// The validated handle, or 'handle.invalid' if handle validation failed 22 final String handle; 23 24 const IdentityInfo({ 25 required this.did, 26 required this.didDoc, 27 required this.handle, 28 }); 29 30 /// Whether the handle is valid (not 'handle.invalid') 31 bool get hasValidHandle => handle != handleInvalid; 32 33 /// Extracts the PDS URL from the DID document. 34 /// 35 /// Returns null if no PDS service is found. 36 String? get pdsUrl => didDoc.extractPdsUrl(); 37} 38 39/// Options for identity resolution. 40class ResolveIdentityOptions { 41 /// Whether to bypass cache 42 final bool noCache; 43 44 /// Cancellation token for the request 45 final CancelToken? cancelToken; 46 47 const ResolveIdentityOptions({this.noCache = false, this.cancelToken}); 48} 49 50/// Interface for resolving atProto identities (handles or DIDs) to complete identity info. 51abstract class IdentityResolver { 52 /// Resolves an identifier (handle or DID) to complete identity information. 53 /// 54 /// The identifier can be either: 55 /// - An atProto handle (e.g., "alice.bsky.social") 56 /// - A DID (e.g., "did:plc:...") 57 /// 58 /// Returns [IdentityInfo] with DID, DID document, and validated handle. 59 Future<IdentityInfo> resolve( 60 String identifier, [ 61 ResolveIdentityOptions? options, 62 ]); 63} 64 65/// Implementation of the official atProto identity resolution strategy. 66/// 67/// This resolver: 68/// 1. Determines if input is a handle or DID 69/// 2. Resolves handle → DID (if needed) 70/// 3. Fetches DID document 71/// 4. Validates bi-directional resolution (handle in DID doc matches original) 72/// 5. Extracts PDS URL from DID document 73/// 74/// This is the **critical piece for decentralization** - it ensures users can 75/// host their data on any PDS, not just bsky.social. 76class AtprotoIdentityResolver implements IdentityResolver { 77 final DidResolver didResolver; 78 final HandleResolver handleResolver; 79 80 AtprotoIdentityResolver({ 81 required this.didResolver, 82 required this.handleResolver, 83 }); 84 85 /// Factory constructor with defaults for typical usage. 86 /// 87 /// [handleResolverUrl] should point to an atProto XRPC service that 88 /// implements com.atproto.identity.resolveHandle. Typically this is 89 /// https://bsky.social for public resolution, or your own PDS. 90 factory AtprotoIdentityResolver.withDefaults({ 91 required String handleResolverUrl, 92 String? plcDirectoryUrl, 93 Dio? dio, 94 DidCache? didCache, 95 HandleCache? handleCache, 96 }) { 97 final dioInstance = dio ?? Dio(); 98 99 final baseDidResolver = AtprotoDidResolver( 100 plcDirectoryUrl: plcDirectoryUrl, 101 dio: dioInstance, 102 ); 103 104 final baseHandleResolver = XrpcHandleResolver( 105 handleResolverUrl, 106 dio: dioInstance, 107 ); 108 109 return AtprotoIdentityResolver( 110 didResolver: CachedDidResolver(baseDidResolver, didCache), 111 handleResolver: CachedHandleResolver(baseHandleResolver, handleCache), 112 ); 113 } 114 115 @override 116 Future<IdentityInfo> resolve( 117 String identifier, [ 118 ResolveIdentityOptions? options, 119 ]) async { 120 return isDid(identifier) 121 ? resolveFromDid(identifier, options) 122 : resolveFromHandle(identifier, options); 123 } 124 125 /// Resolves identity starting from a DID. 126 /// 127 /// This: 128 /// 1. Fetches the DID document 129 /// 2. Extracts the handle from alsoKnownAs 130 /// 3. Validates that the handle resolves back to the same DID 131 Future<IdentityInfo> resolveFromDid( 132 String did, [ 133 ResolveIdentityOptions? options, 134 ]) async { 135 final document = await getDocumentFromDid(did, options); 136 137 // We will only return the document's handle alias if it resolves to the 138 // same DID as the input (bi-directional validation) 139 final handle = document.extractNormalizedHandle(); 140 String? resolvedDid; 141 142 if (handle != null) { 143 try { 144 resolvedDid = await handleResolver.resolve( 145 handle, 146 ResolveHandleOptions( 147 noCache: options?.noCache ?? false, 148 cancelToken: options?.cancelToken, 149 ), 150 ); 151 } catch (e) { 152 // Ignore errors (handle might be temporarily unavailable) 153 resolvedDid = null; 154 } 155 } 156 157 return IdentityInfo( 158 did: document.id, 159 didDoc: document, 160 handle: handle != null && resolvedDid == did ? handle : handleInvalid, 161 ); 162 } 163 164 /// Resolves identity starting from a handle. 165 /// 166 /// This: 167 /// 1. Resolves handle → DID 168 /// 2. Fetches DID document 169 /// 3. Validates that the DID document contains the original handle 170 Future<IdentityInfo> resolveFromHandle( 171 String handle, [ 172 ResolveIdentityOptions? options, 173 ]) async { 174 final document = await getDocumentFromHandle(handle, options); 175 176 // Bi-directional resolution is enforced in getDocumentFromHandle() 177 return IdentityInfo( 178 did: document.id, 179 didDoc: document, 180 handle: document.extractNormalizedHandle() ?? handleInvalid, 181 ); 182 } 183 184 /// Fetches a DID document from a DID. 185 Future<DidDocument> getDocumentFromDid( 186 String did, [ 187 ResolveIdentityOptions? options, 188 ]) async { 189 return didResolver.resolve( 190 did, 191 ResolveDidOptions( 192 noCache: options?.noCache ?? false, 193 cancelToken: options?.cancelToken, 194 ), 195 ); 196 } 197 198 /// Fetches a DID document from a handle with bi-directional validation. 199 /// 200 /// This method: 201 /// 1. Normalizes and validates the handle 202 /// 2. Resolves handle → DID 203 /// 3. Fetches DID document 204 /// 4. Verifies the DID document contains the original handle 205 Future<DidDocument> getDocumentFromHandle( 206 String input, [ 207 ResolveIdentityOptions? options, 208 ]) async { 209 final handle = asNormalizedHandle(input); 210 if (handle == null) { 211 throw InvalidHandleError(input, 'Invalid handle format'); 212 } 213 214 final did = await handleResolver.resolve( 215 handle, 216 ResolveHandleOptions( 217 noCache: options?.noCache ?? false, 218 cancelToken: options?.cancelToken, 219 ), 220 ); 221 222 if (did == null) { 223 throw IdentityResolverError('Handle "$handle" does not resolve to a DID'); 224 } 225 226 // Fetch the DID document 227 final document = await didResolver.resolve( 228 did, 229 ResolveDidOptions( 230 noCache: options?.noCache ?? false, 231 cancelToken: options?.cancelToken, 232 ), 233 ); 234 235 // Enforce bi-directional resolution 236 final docHandle = document.extractNormalizedHandle(); 237 if (handle != docHandle) { 238 throw IdentityResolverError( 239 'DID document for "$did" does not include the handle "$handle" ' 240 '(found: ${docHandle ?? "none"})', 241 ); 242 } 243 244 return document; 245 } 246 247 /// Convenience method to resolve directly to PDS URL. 248 /// 249 /// This is the most common use case: given a handle or DID, find the PDS URL. 250 Future<String> resolveToPds( 251 String identifier, [ 252 ResolveIdentityOptions? options, 253 ]) async { 254 final info = await resolve(identifier, options); 255 final pdsUrl = info.pdsUrl; 256 257 if (pdsUrl == null) { 258 throw IdentityResolverError( 259 'No PDS endpoint found in DID document for $identifier', 260 ); 261 } 262 263 return pdsUrl; 264 } 265} 266 267/// Options for creating an identity resolver. 268class IdentityResolverOptions { 269 /// Custom identity resolver (if not provided, AtprotoIdentityResolver is used) 270 final IdentityResolver? identityResolver; 271 272 /// Custom DID resolver 273 final DidResolver? didResolver; 274 275 /// Custom handle resolver (or URL string for XRPC resolver) 276 final dynamic handleResolver; // HandleResolver, String, or Uri 277 278 /// Custom DID cache 279 final DidCache? didCache; 280 281 /// Custom handle cache 282 final HandleCache? handleCache; 283 284 /// Custom Dio instance for HTTP requests 285 final Dio? dio; 286 287 /// PLC directory URL (defaults to https://plc.directory/) 288 final String? plcDirectoryUrl; 289 290 const IdentityResolverOptions({ 291 this.identityResolver, 292 this.didResolver, 293 this.handleResolver, 294 this.didCache, 295 this.handleCache, 296 this.dio, 297 this.plcDirectoryUrl, 298 }); 299} 300 301/// Creates an identity resolver with the given options. 302/// 303/// This is the main entry point for creating an identity resolver. 304/// It handles setting up default implementations with proper caching. 305IdentityResolver createIdentityResolver(IdentityResolverOptions options) { 306 // If a custom identity resolver is provided, use it 307 if (options.identityResolver != null) { 308 return options.identityResolver!; 309 } 310 311 final dioInstance = options.dio ?? Dio(); 312 313 // Create DID resolver 314 final didResolver = _createDidResolver(options, dioInstance); 315 316 // Create handle resolver 317 final handleResolver = _createHandleResolver(options, dioInstance); 318 319 return AtprotoIdentityResolver( 320 didResolver: didResolver, 321 handleResolver: handleResolver, 322 ); 323} 324 325DidResolver _createDidResolver(IdentityResolverOptions options, Dio dio) { 326 final didResolver = 327 options.didResolver ?? 328 AtprotoDidResolver(plcDirectoryUrl: options.plcDirectoryUrl, dio: dio); 329 330 // Wrap with cache if not already cached 331 if (didResolver is CachedDidResolver && options.didCache == null) { 332 return didResolver; 333 } 334 335 return CachedDidResolver(didResolver, options.didCache); 336} 337 338HandleResolver _createHandleResolver(IdentityResolverOptions options, Dio dio) { 339 final handleResolverInput = options.handleResolver; 340 341 if (handleResolverInput == null) { 342 throw ArgumentError( 343 'handleResolver is required. Provide either a HandleResolver instance, ' 344 'a URL string, or a Uri pointing to an XRPC service.', 345 ); 346 } 347 348 HandleResolver baseResolver; 349 350 if (handleResolverInput is HandleResolver) { 351 baseResolver = handleResolverInput; 352 } else if (handleResolverInput is String || handleResolverInput is Uri) { 353 baseResolver = XrpcHandleResolver(handleResolverInput.toString(), dio: dio); 354 } else { 355 throw ArgumentError( 356 'handleResolver must be a HandleResolver, String, or Uri', 357 ); 358 } 359 360 // Wrap with cache if not already cached 361 if (baseResolver is CachedHandleResolver && options.handleCache == null) { 362 return baseResolver; 363 } 364 365 return CachedHandleResolver(baseResolver, options.handleCache); 366}