0
# Identity Resolution
1
2
DID document resolution, caching mechanisms, and AT Protocol-specific identity data extraction for decentralized identity management. These components enable efficient resolution of decentralized identifiers and handles within the AT Protocol ecosystem.
3
4
## Capabilities
5
6
### Identity Resolvers
7
8
Resolve decentralized identifiers (DIDs) and handles to their corresponding DID documents and service endpoints.
9
10
#### Synchronous Identity Resolver
11
12
```python { .api }
13
class IdResolver:
14
"""
15
Identity resolver for DIDs and handles.
16
17
Resolves both DID and handle identities with configurable PLC directory
18
and optional caching support.
19
"""
20
def __init__(self,
21
plc_url: Optional[str] = None,
22
timeout: Optional[float] = None,
23
cache: Optional['DidBaseCache'] = None):
24
"""
25
Initialize the identity resolver.
26
27
Args:
28
plc_url (str, optional): PLC directory URL (default: https://plc.directory)
29
timeout (float, optional): Request timeout in seconds
30
cache (DidBaseCache, optional): DID document cache implementation
31
"""
32
33
def resolve_did(self, did: str) -> 'DidDocument':
34
"""
35
Resolve DID to DID document.
36
37
Args:
38
did (str): DID to resolve (e.g., "did:plc:alice123")
39
40
Returns:
41
DidDocument: Resolved DID document
42
43
Raises:
44
ResolutionError: If DID cannot be resolved
45
NetworkError: If resolution service is unreachable
46
"""
47
48
def resolve_handle(self, handle: str) -> str:
49
"""
50
Resolve handle to DID.
51
52
Args:
53
handle (str): Handle to resolve (e.g., "alice.bsky.social")
54
55
Returns:
56
str: Resolved DID
57
58
Raises:
59
ResolutionError: If handle cannot be resolved
60
"""
61
62
def resolve_atproto_data(self, identifier: str) -> 'AtprotoData':
63
"""
64
Resolve identifier to AT Protocol-specific data.
65
66
Args:
67
identifier (str): DID or handle to resolve
68
69
Returns:
70
AtprotoData: Extracted AT Protocol data
71
"""
72
73
def get_pds_endpoint(self, identifier: str) -> Optional[str]:
74
"""
75
Get Personal Data Server endpoint for identifier.
76
77
Args:
78
identifier (str): DID or handle
79
80
Returns:
81
Optional[str]: PDS endpoint URL or None if not found
82
"""
83
84
def refresh_cache_entry(self, did: str):
85
"""
86
Force refresh of cached DID document.
87
88
Args:
89
did (str): DID to refresh in cache
90
"""
91
```
92
93
#### Asynchronous Identity Resolver
94
95
```python { .api }
96
class AsyncIdResolver:
97
"""
98
Asynchronous identity resolver for DIDs and handles.
99
100
Async version of identity resolver for non-blocking operations.
101
"""
102
def __init__(self,
103
plc_url: Optional[str] = None,
104
timeout: Optional[float] = None,
105
cache: Optional['AsyncDidBaseCache'] = None):
106
"""
107
Initialize the async identity resolver.
108
109
Args:
110
plc_url (str, optional): PLC directory URL
111
timeout (float, optional): Request timeout in seconds
112
cache (AsyncDidBaseCache, optional): Async DID document cache
113
"""
114
115
async def resolve_did(self, did: str) -> 'DidDocument':
116
"""
117
Resolve DID to DID document asynchronously.
118
119
Args:
120
did (str): DID to resolve
121
122
Returns:
123
DidDocument: Resolved DID document
124
"""
125
126
async def resolve_handle(self, handle: str) -> str:
127
"""
128
Resolve handle to DID asynchronously.
129
130
Args:
131
handle (str): Handle to resolve
132
133
Returns:
134
str: Resolved DID
135
"""
136
137
async def resolve_atproto_data(self, identifier: str) -> 'AtprotoData':
138
"""
139
Resolve identifier to AT Protocol data asynchronously.
140
141
Args:
142
identifier (str): DID or handle to resolve
143
144
Returns:
145
AtprotoData: Extracted AT Protocol data
146
"""
147
148
async def close(self):
149
"""Close the async resolver connections."""
150
```
151
152
Usage examples:
153
154
```python
155
from atproto import IdResolver, AtprotoData
156
157
# Initialize resolver with default settings
158
resolver = IdResolver()
159
160
# Resolve DID to document
161
did = "did:plc:alice123456789"
162
doc = resolver.resolve_did(did)
163
print(f"DID document ID: {doc.id}")
164
165
# Resolve handle to DID
166
handle = "alice.bsky.social"
167
resolved_did = resolver.resolve_handle(handle)
168
print(f"Handle {handle} resolves to: {resolved_did}")
169
170
# Get AT Protocol specific data
171
atproto_data = resolver.resolve_atproto_data(handle)
172
print(f"PDS endpoint: {atproto_data.pds}")
173
print(f"Signing key: {atproto_data.signing_key}")
174
175
# Get PDS endpoint directly
176
pds_endpoint = resolver.get_pds_endpoint("alice.bsky.social")
177
if pds_endpoint:
178
print(f"Alice's PDS: {pds_endpoint}")
179
```
180
181
```python
182
import asyncio
183
from atproto import AsyncIdResolver
184
185
async def resolve_identities():
186
resolver = AsyncIdResolver()
187
188
# Resolve multiple identities concurrently
189
identities = [
190
"alice.bsky.social",
191
"bob.bsky.social",
192
"did:plc:charlie789"
193
]
194
195
tasks = [resolver.resolve_atproto_data(identity) for identity in identities]
196
results = await asyncio.gather(*tasks, return_exceptions=True)
197
198
for identity, result in zip(identities, results):
199
if isinstance(result, Exception):
200
print(f"Failed to resolve {identity}: {result}")
201
else:
202
print(f"{identity} -> {result.did} (PDS: {result.pds})")
203
204
await resolver.close()
205
206
asyncio.run(resolve_identities())
207
```
208
209
### Caching
210
211
Efficient caching mechanisms for DID documents to reduce resolution latency and network overhead.
212
213
#### Synchronous In-Memory Cache
214
215
```python { .api }
216
class DidInMemoryCache:
217
"""
218
In-memory cache for DID documents.
219
220
Provides fast access to frequently resolved DID documents with
221
configurable TTL and size limits.
222
"""
223
def __init__(self, max_size: int = 1000, ttl_seconds: int = 3600):
224
"""
225
Initialize the cache.
226
227
Args:
228
max_size (int): Maximum number of cached documents
229
ttl_seconds (int): Time-to-live for cache entries in seconds
230
"""
231
232
def get(self, did: str) -> Optional['DidDocument']:
233
"""
234
Get DID document from cache.
235
236
Args:
237
did (str): DID to look up
238
239
Returns:
240
Optional[DidDocument]: Cached document or None if not found/expired
241
"""
242
243
def set(self, did: str, document: 'DidDocument'):
244
"""
245
Store DID document in cache.
246
247
Args:
248
did (str): DID key
249
document (DidDocument): Document to cache
250
"""
251
252
def delete(self, did: str):
253
"""
254
Remove DID document from cache.
255
256
Args:
257
did (str): DID to remove
258
"""
259
260
def clear(self):
261
"""Clear all cached documents."""
262
263
def refresh(self, did: str, get_doc_callback: 'GetDocCallback'):
264
"""
265
Refresh cached document by fetching new version.
266
267
Args:
268
did (str): DID to refresh
269
get_doc_callback (GetDocCallback): Function to fetch fresh document
270
"""
271
272
def get_stats(self) -> Dict[str, Any]:
273
"""
274
Get cache statistics.
275
276
Returns:
277
Dict[str, Any]: Cache statistics (hits, misses, size, etc.)
278
"""
279
```
280
281
#### Asynchronous In-Memory Cache
282
283
```python { .api }
284
class AsyncDidInMemoryCache:
285
"""
286
Asynchronous in-memory cache for DID documents.
287
288
Async version of DID document cache with thread-safe operations.
289
"""
290
def __init__(self, max_size: int = 1000, ttl_seconds: int = 3600):
291
"""
292
Initialize the async cache.
293
294
Args:
295
max_size (int): Maximum number of cached documents
296
ttl_seconds (int): Time-to-live for cache entries
297
"""
298
299
async def get(self, did: str) -> Optional['DidDocument']:
300
"""
301
Get DID document from cache asynchronously.
302
303
Args:
304
did (str): DID to look up
305
306
Returns:
307
Optional[DidDocument]: Cached document or None
308
"""
309
310
async def set(self, did: str, document: 'DidDocument'):
311
"""
312
Store DID document in cache asynchronously.
313
314
Args:
315
did (str): DID key
316
document (DidDocument): Document to cache
317
"""
318
319
async def refresh(self, did: str, get_doc_callback: 'AsyncGetDocCallback'):
320
"""
321
Refresh cached document asynchronously.
322
323
Args:
324
did (str): DID to refresh
325
get_doc_callback (AsyncGetDocCallback): Async function to fetch document
326
"""
327
```
328
329
Usage examples:
330
331
```python
332
from atproto import IdResolver, DidInMemoryCache
333
334
# Create resolver with cache
335
cache = DidInMemoryCache(max_size=500, ttl_seconds=1800) # 30 minute TTL
336
resolver = IdResolver(cache=cache)
337
338
# First resolution - cache miss
339
print("First resolution (cache miss)")
340
start_time = time.time()
341
doc1 = resolver.resolve_did("did:plc:alice123")
342
print(f"Resolved in {time.time() - start_time:.3f}s")
343
344
# Second resolution - cache hit
345
print("Second resolution (cache hit)")
346
start_time = time.time()
347
doc2 = resolver.resolve_did("did:plc:alice123")
348
print(f"Resolved in {time.time() - start_time:.3f}s")
349
350
# Check cache statistics
351
stats = cache.get_stats()
352
print(f"Cache hits: {stats['hits']}, misses: {stats['misses']}")
353
354
# Force refresh of cache entry
355
cache.refresh("did:plc:alice123", lambda did: resolver.resolve_did(did))
356
```
357
358
### AT Protocol Data Extraction
359
360
Extract AT Protocol-specific information from DID documents for efficient client operations.
361
362
#### AtprotoData Class
363
364
```python { .api }
365
class AtprotoData:
366
"""
367
ATProtocol-specific data extracted from DID documents.
368
369
Attributes:
370
did (str): DID identifier
371
signing_key (Optional[str]): Signing key for the identity
372
handle (Optional[str]): Associated handle
373
pds (Optional[str]): Personal Data Server endpoint
374
pds_active (bool): Whether PDS is active
375
declarations (Dict[str, Any]): Additional declarations
376
"""
377
did: str
378
signing_key: Optional[str]
379
handle: Optional[str]
380
pds: Optional[str]
381
pds_active: bool
382
declarations: Dict[str, Any]
383
384
@classmethod
385
def from_did_doc(cls, did_doc: 'DidDocument') -> 'AtprotoData':
386
"""
387
Extract AT Protocol data from DID document.
388
389
Args:
390
did_doc (DidDocument): DID document to extract from
391
392
Returns:
393
AtprotoData: Extracted AT Protocol data
394
395
Raises:
396
ExtractionError: If required data cannot be extracted
397
"""
398
399
def get_service_endpoint(self, service_id: str) -> Optional[str]:
400
"""
401
Get service endpoint by ID.
402
403
Args:
404
service_id (str): Service identifier
405
406
Returns:
407
Optional[str]: Service endpoint URL
408
"""
409
410
def has_valid_pds(self) -> bool:
411
"""
412
Check if the identity has a valid PDS.
413
414
Returns:
415
bool: True if PDS is available and active
416
"""
417
418
def get_signing_key_multikey(self) -> Optional['Multikey']:
419
"""
420
Get signing key as Multikey object.
421
422
Returns:
423
Optional[Multikey]: Signing key or None if not available
424
"""
425
426
def to_dict(self) -> Dict[str, Any]:
427
"""
428
Convert to dictionary representation.
429
430
Returns:
431
Dict[str, Any]: Dictionary representation
432
"""
433
```
434
435
Usage examples:
436
437
```python
438
from atproto import IdResolver, AtprotoData, Multikey
439
440
resolver = IdResolver()
441
442
# Extract AT Protocol data
443
handle = "alice.bsky.social"
444
atproto_data = resolver.resolve_atproto_data(handle)
445
446
print(f"DID: {atproto_data.did}")
447
print(f"Handle: {atproto_data.handle}")
448
print(f"PDS: {atproto_data.pds}")
449
print(f"PDS Active: {atproto_data.pds_active}")
450
451
# Check if identity has valid PDS
452
if atproto_data.has_valid_pds():
453
print("✓ Identity has valid PDS")
454
455
# Get signing key
456
if atproto_data.signing_key:
457
signing_key = atproto_data.get_signing_key_multikey()
458
if signing_key:
459
print(f"Signing algorithm: {signing_key.jwt_alg}")
460
461
# Convert to dictionary for storage/serialization
462
data_dict = atproto_data.to_dict()
463
print(f"Serialized data: {data_dict}")
464
465
# Create from DID document directly
466
did_doc = resolver.resolve_did(atproto_data.did)
467
extracted_data = AtprotoData.from_did_doc(did_doc)
468
assert extracted_data.did == atproto_data.did
469
```
470
471
### Resolution Utilities
472
473
Utility functions for identity resolution and validation.
474
475
```python { .api }
476
def is_valid_did(did: str) -> bool:
477
"""
478
Validate DID format.
479
480
Args:
481
did (str): DID to validate
482
483
Returns:
484
bool: True if valid DID format
485
"""
486
487
def is_valid_handle(handle: str) -> bool:
488
"""
489
Validate handle format.
490
491
Args:
492
handle (str): Handle to validate
493
494
Returns:
495
bool: True if valid handle format
496
"""
497
498
def normalize_identifier(identifier: str) -> str:
499
"""
500
Normalize DID or handle identifier.
501
502
Args:
503
identifier (str): Identifier to normalize
504
505
Returns:
506
str: Normalized identifier
507
"""
508
509
def extract_handle_from_did_doc(did_doc: DidDocument) -> Optional[str]:
510
"""
511
Extract handle from DID document alsoKnownAs field.
512
513
Args:
514
did_doc (DidDocument): DID document
515
516
Returns:
517
Optional[str]: Extracted handle or None
518
"""
519
```
520
521
Usage examples:
522
523
```python
524
from atproto import (
525
is_valid_did, is_valid_handle, normalize_identifier,
526
extract_handle_from_did_doc, IdResolver
527
)
528
529
# Validate identifiers
530
identifiers = [
531
"did:plc:alice123456789",
532
"alice.bsky.social",
533
"invalid-identifier",
534
"@alice.bsky.social" # Should be normalized
535
]
536
537
for identifier in identifiers:
538
print(f"'{identifier}':")
539
print(f" Valid DID: {is_valid_did(identifier)}")
540
print(f" Valid handle: {is_valid_handle(identifier)}")
541
print(f" Normalized: {normalize_identifier(identifier)}")
542
print()
543
544
# Extract handle from DID document
545
resolver = IdResolver()
546
did_doc = resolver.resolve_did("did:plc:alice123")
547
handle = extract_handle_from_did_doc(did_doc)
548
if handle:
549
print(f"Handle from DID doc: {handle}")
550
```
551
552
### Error Handling
553
554
```python { .api }
555
class ResolutionError(Exception):
556
"""Base exception for identity resolution errors."""
557
558
class DidNotFoundError(ResolutionError):
559
"""Raised when DID cannot be found."""
560
561
class HandleNotFoundError(ResolutionError):
562
"""Raised when handle cannot be resolved."""
563
564
class InvalidIdentifierError(ResolutionError):
565
"""Raised when identifier format is invalid."""
566
567
class ExtractionError(ResolutionError):
568
"""Raised when AT Protocol data cannot be extracted."""
569
570
class CacheError(Exception):
571
"""Base exception for cache operations."""
572
```
573
574
Robust resolution with error handling:
575
576
```python
577
from atproto import (
578
IdResolver, ResolutionError, DidNotFoundError,
579
HandleNotFoundError, InvalidIdentifierError
580
)
581
582
def safe_resolve_identity(resolver, identifier):
583
"""Safely resolve identity with comprehensive error handling."""
584
try:
585
# Validate identifier format first
586
if not (is_valid_did(identifier) or is_valid_handle(identifier)):
587
raise InvalidIdentifierError(f"Invalid identifier format: {identifier}")
588
589
# Normalize identifier
590
normalized = normalize_identifier(identifier)
591
592
# Resolve to AT Protocol data
593
atproto_data = resolver.resolve_atproto_data(normalized)
594
595
return {
596
'success': True,
597
'data': atproto_data,
598
'original': identifier,
599
'normalized': normalized
600
}
601
602
except DidNotFoundError:
603
return {'success': False, 'error': 'DID not found', 'identifier': identifier}
604
except HandleNotFoundError:
605
return {'success': False, 'error': 'Handle not found', 'identifier': identifier}
606
except InvalidIdentifierError as e:
607
return {'success': False, 'error': str(e), 'identifier': identifier}
608
except ResolutionError as e:
609
return {'success': False, 'error': f'Resolution failed: {e}', 'identifier': identifier}
610
except Exception as e:
611
return {'success': False, 'error': f'Unexpected error: {e}', 'identifier': identifier}
612
613
# Usage
614
resolver = IdResolver()
615
identifiers = ["alice.bsky.social", "invalid-id", "did:plc:alice123"]
616
617
for identifier in identifiers:
618
result = safe_resolve_identity(resolver, identifier)
619
if result['success']:
620
print(f"✓ {identifier} -> {result['data'].did}")
621
else:
622
print(f"✗ {identifier}: {result['error']}")
623
```