Independent DID PLC Directory client for Dart 🦋

1. Guide 🌎

A high-performance, independent Dart library for interacting with DID PLC Directory services. This library provides a complete implementation of the DID PLC specification with zero external dependencies on atproto packages.

1.1. Features ⭐

  • Zero atproto Dependencies - Completely independent implementation
  • High Performance - Built-in caching, batching, and streaming support
  • Type Safe - Full type safety with freezed data classes
  • Comprehensive - Complete DID PLC specification support
  • Well Tested - 100% test coverage with comprehensive test suite
  • Modern Dart - Uses latest Dart features and null safety
  • Simple API - Clean and intuitive interface for DID operations
  • Efficient Caching - Built-in memory cache with TTL support
  • Batch Processing - Process multiple DIDs efficiently

1.2. Installation 📦

Add this to your package's pubspec.yaml file:

dependencies:
  did_plc: ^1.1.2

Then run:

dart pub get

1.3. Quick Start 🚀

import 'package:did_plc/did_plc.dart';

Future<void> main() async {
  // Create a PLC client
  final plc = PLC();

  try {
    // Fetch a DID document
    final document = await plc.getDocument('did:plc:iijrtk7ocored6zuziwmqq3c');
    print('DID Document: ${document.id}');
    
    // Get operation log
    final operationLog = await plc.getOperationLog('did:plc:iijrtk7ocored6zuziwmqq3c');
    print('Operations: ${operationLog.log.length}');
    
  } on NetworkException catch (e) {
    print('Network error: ${e.message}');
  } on PlcException catch (e) {
    print('PLC error: ${e.message}');
  } finally {
    // Always close the client
    plc.close();
  }
}

1.4. API Overview 📚

Core Operations

final plc = PLC();

// Document operations
final document = await plc.getDocument(did);
final documentData = await plc.getDocumentData(did);

// Operation log operations
final operationLog = await plc.getOperationLog(did);
final auditLog = await plc.getAuditableLog(did);
final lastOperation = await plc.getLastOp(did);

// Batch operations
final documents = await plc.getDocuments([did1, did2, did3]);

// Health check
final health = await plc.health();

Advanced Features

// Custom configuration
final plc = PLC(
  service: 'https://plc.directory',
  cacheManager: CacheManager(
    CachePolicy(
      ttl: Duration(minutes: 10),
      maxSize: 1000,
      enableLru: true,
    ),
  ),
  retryPolicy: RetryPolicy(
    maxAttempts: 3,
    initialDelay: Duration(seconds: 1),
    backoffMultiplier: 2.0,
  ),
);

// Streaming large datasets
await for (final operation in plc.exportOpsStream()) {
  print('Processing: ${operation.did}');
}

1.5. Advanced Features 🔧

Caching

Built-in intelligent caching reduces network requests and improves performance:

final plc = PLC(
  cacheManager: CacheManager(
    CachePolicy(
      ttl: Duration(minutes: 15),    // Cache for 15 minutes
      maxSize: 2000,                 // Maximum 2000 entries
      enableLru: true,               // LRU eviction
    ),
  ),
);

// Remember to call `plc.close()` when done — it also disposes the cache
// manager and releases its resources.

Batch Processing

Efficiently process multiple DIDs in parallel:

final dids = ['did:plc:abc123', 'did:plc:def456', 'did:plc:ghi789'];
final documents = await plc.getDocuments(dids);

// Process results
for (final entry in documents.entries) {
  print('${entry.key}: ${entry.value.id}');
}

Streaming

Handle large datasets efficiently with streaming:

await for (final entry in plc.exportOpsStream(
  after: DateTime.now().subtract(Duration(days: 1)),
  count: 1000,
)) {
  // Process each entry as it arrives
  print('Processing: ${entry.did}');
}

Resource Management

Proper resource management:

final plc = PLC();
try {
  final document = await plc.getDocument('did:plc:example');
  print('Document: ${document.id}');
} finally {
  // Always close the client
  plc.close();
}

Cryptography & Operation Building

Since v1.1.0 the package ships real ECDSA cryptography (secp256k1 / P-256, with RFC 6979 deterministic k, low-S normalization, DAG-CBOR canonicalization, and correct did:plc derivation) together with fluent builders for constructing operations and DID documents. All of the following are exported from package:did_plc/did_plc.dart:

  • OperationBuilder — fluent builder for a PLC operation map (OperationBuilder.create() / .update(), then build() / buildUnsafe()).
  • DidBuilder — fluent builder for a DID document.
  • CryptoKey / KeyManager — key material and did:key handling. KeyType.secp256k1 and KeyType.p256 are supported.
  • PlcSigner — signs an operation (signOperation, signRawOperation).
  • PlcVerifier — verifies a signature or an operation chain (verifyOperation, verifyRawOperation, verifyOperationChain, verifyAuditLog).
// Generate a signing key and derive its did:key.
final signingKey = CryptoKey.generate(KeyType.secp256k1);
final signer = PlcSigner();
final verifier = PlcVerifier();

// Build an unsigned operation.
final unsigned = OperationBuilder.create()
    .rotationKeys([signingKey.toDidKey()])
    .addVerificationMethod('atproto', signingKey.toDidKey())
    .addAlsoKnownAs('at://alice.example.com')
    .addService(
      'atproto_pds',
      {'type': 'AtprotoPersonalDataServer', 'endpoint': 'https://pds.example.com'},
    )
    .build();

// Sign the raw operation map and verify it against the rotation keys.
final signature = signer.signRawOperation(unsigned, signingKey);
final signed = {...unsigned, 'sig': signature};
final result = verifier.verifyRawOperation(signed, [signingKey.toDidKey()]);
print('Valid: ${result.isValid}');

See example/crypto_example.dart for a full walkthrough of analyzing keys and operations.

1.6. Performance Best Practices 🚄

1. Use Caching Effectively

// Configure appropriate cache settings
final plc = PLC(
  cacheManager: CacheManager(
    CachePolicy(
      ttl: Duration(minutes: 10),  // Adjust based on your use case
      maxSize: 1000,               // Prevent memory bloat
    ),
  ),
);

2. Batch Multiple Requests

// Instead of multiple individual requests
final doc1 = await plc.getDocument(did1);
final doc2 = await plc.getDocument(did2);
final doc3 = await plc.getDocument(did3);

// Use batch processing
final documents = await plc.getDocuments([did1, did2, did3]);

3. Use Streaming for Large Datasets

// For processing large amounts of data
await for (final operation in plc.exportOpsStream(count: 10000)) {
  // Process incrementally to avoid memory issues
  print('Processing: ${operation.did}');
}

4. Configure Retry Policies

final plc = PLC(
  retryPolicy: RetryPolicy(
    maxAttempts: 3,
    initialDelay: Duration(milliseconds: 500),
    backoffMultiplier: 2.0,
  ),
);

5. Proper Resource Management

Future<void> processDocuments() async {
  final plc = PLC();
  try {
    // Your processing logic
    await plc.getDocument(did);
  } finally {
    // Always close to free resources
    plc.close();
  }
}


1.7. Examples 💡

See the examples directory for complete working examples:

For more detailed documentation, see:

Libraries

did_plc