or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

client-operations.mdcore-functionality.mdcryptographic-operations.mdidentity-resolution.mdindex.mdjwt-operations.mdreal-time-streaming.md

identity-resolution.mddocs/

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

```