/** * Basic Usage Example for @atpkeyserver/client * * This example demonstrates: * 1. Crypto functions only (no keyserver) * 2. KeyserverClient with encryption * 3. KeyserverClient with decryption * 4. Error handling patterns * * Prerequisites: * - Node.js 22+ or Bun * - @atpkeyserver/client installed * - @atproto/api installed (for service auth) * * Run: bun run examples/basic-usage.ts */ import { encryptMessage, decryptMessage, KeyserverClient, ForbiddenError, NotFoundError, DecryptionError, NetworkError, } from '../src/index' import { AtpAgent } from '@atproto/api' // ============================================================================ // Example 1: Crypto Functions Only (No Keyserver) // ============================================================================ function example1_cryptoOnly() { console.log('\n=== Example 1: Crypto Functions Only ===\n') // Message ID (AT-URI) used as Additional Authenticated Data (AAD) const messageId = 'at://did:plc:abc123/app.bsky.feed.post/xyz789' // 32-byte secret key (64 hex characters) const secretKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' // Plaintext message const plaintext = JSON.stringify({ text: 'Secret message for followers', createdAt: new Date().toISOString(), }) console.log('Original plaintext:', plaintext) // Encrypt const ciphertext = encryptMessage(messageId, secretKey, plaintext) console.log('Encrypted (hex):', ciphertext.slice(0, 64) + '...') // Decrypt const decrypted = decryptMessage(messageId, secretKey, ciphertext) console.log('Decrypted:', decrypted) console.log('āœ… Crypto round-trip successful!') } // ============================================================================ // Example 2: KeyserverClient with Encryption // ============================================================================ async function example2_keyserverEncryption() { console.log('\n=== Example 2: KeyserverClient Encryption ===\n') // Setup PDS client const agent = new AtpAgent({ service: 'https://bsky.social' }) // In real usage, authenticate with user credentials: // await agent.login({ // identifier: 'user.bsky.social', // password: 'app-password' // }) // For this example, we'll use a mock setup console.log('āš ļø Skipping PDS login (example only)') const mockDid = 'did:plc:example123' const mockServiceAuthToken = 'mock_jwt_token' // Create keyserver client const keyserver = new KeyserverClient({ keyserverDid: 'did:web:keyserver.example.com', // Service auth token provider getServiceAuthToken: async (aud: string, lxm?: string) => { console.log(`Getting service auth token for aud=${aud}, lxm=${lxm}`) // In real usage, call PDS: // const { data } = await agent.com.atproto.server.getServiceAuth({ // aud, // lxm, // exp: Math.floor(Date.now() / 1000) + 60 // }) // return data.token return mockServiceAuthToken }, }) // Encrypt a post const groupId = `${mockDid}#followers` const postUri = 'at://did:plc:abc123/app.bsky.feed.post/post1' const plaintext = JSON.stringify({ text: 'Secret message for followers only', createdAt: new Date().toISOString(), }) console.log('Encrypting for group:', groupId) console.log('Post URI:', postUri) try { const { ciphertext, version } = await keyserver.encrypt( postUri, groupId, plaintext, ) console.log('āœ… Encrypted successfully!') console.log('Ciphertext:', ciphertext.slice(0, 64) + '...') console.log('Key version:', version) // In real usage, store this in your PDS: // await agent.com.atproto.repo.putRecord({ // repo: mockDid, // collection: 'app.bsky.feed.post', // record: { // encrypted_content: ciphertext, // key_version: version, // encrypted_at: new Date().toISOString(), // visibility: 'followers' // } // }) console.log( 'šŸ“ In production, store encrypted_content + key_version in PDS', ) } catch (error) { console.error('āŒ Encryption failed:', error) } // Clear cache when done keyserver.clearCache() } // ============================================================================ // Example 3: KeyserverClient with Decryption // ============================================================================ async function example3_keyserverDecryption() { console.log('\n=== Example 3: KeyserverClient Decryption ===\n') // Setup (same as example 2) const mockServiceAuthToken = 'mock_jwt_token' const keyserver = new KeyserverClient({ keyserverDid: 'did:web:keyserver.example.com', getServiceAuthToken: async () => mockServiceAuthToken, }) // Simulated encrypted post from feed const encryptedPost = { uri: 'at://did:plc:abc123/app.bsky.feed.post/post1', author: 'did:plc:abc123', encrypted_content: '0123456789abcdef0123456789abcdef0123456789abcdef...', key_version: 2, visibility: 'followers', } const groupId = `${encryptedPost.author}#${encryptedPost.visibility}` console.log('Decrypting post:', encryptedPost.uri) console.log('Group:', groupId) try { const plaintext = await keyserver.decrypt( encryptedPost.uri, groupId, encryptedPost.encrypted_content, encryptedPost.key_version, ) const decrypted = JSON.parse(plaintext) console.log('āœ… Decrypted successfully!') console.log('Content:', decrypted) } catch (error) { console.error('āŒ Decryption failed (expected in mock environment)') console.error('Error:', error instanceof Error ? error.message : error) } keyserver.clearCache() } // ============================================================================ // Example 4: Error Handling Patterns // ============================================================================ async function example4_errorHandling() { console.log('\n=== Example 4: Error Handling ===\n') const mockServiceAuthToken = 'mock_jwt_token' const keyserver = new KeyserverClient({ keyserverDid: 'did:web:keyserver.example.com', getServiceAuthToken: async () => mockServiceAuthToken, }) // Simulated encrypted post const post = { uri: 'at://did:plc:abc123/app.bsky.feed.post/post1', groupId: 'did:plc:abc123#followers', ciphertext: 'mock_ciphertext', version: 2, } try { const plaintext = await keyserver.decrypt( post.uri, post.groupId, post.ciphertext, post.version, ) console.log('Decrypted:', plaintext) } catch (error) { // Handle specific error types if (error instanceof ForbiddenError) { console.log('🚫 ForbiddenError: User lost access to this group') console.log('→ Display: "You no longer have access to this content"') } else if (error instanceof NotFoundError) { console.log('šŸ” NotFoundError: Group was deleted') console.log('→ Display: "This group no longer exists"') } else if (error instanceof DecryptionError) { console.log('šŸ” DecryptionError: Cannot decrypt (corrupted/wrong key)') console.log('→ Display: "Cannot decrypt this message"') console.log('→ Do NOT retry - this is permanent') } else if (error instanceof NetworkError) { console.log('🌐 NetworkError: Temporary network issue') console.log('→ Can retry with exponential backoff') console.log( `→ Status: ${(error as NetworkError).statusCode || 'unknown'}`, ) } else { console.log('ā“ Unknown error:', error) console.log('→ Log for investigation and show generic error to user') } } keyserver.clearCache() } // ============================================================================ // Example 5: Cache Management // ============================================================================ async function example5_cacheManagement() { console.log('\n=== Example 5: Cache Management ===\n') const keyserver = new KeyserverClient({ keyserverDid: 'did:web:keyserver.example.com', getServiceAuthToken: async () => 'mock_token', cache: { maxSize: 1000, // Max 1000 cached keys activeKeyTtl: 3600, // Active keys cached for 1 hour historicalKeyTtl: 86400, // Historical keys cached for 24 hours }, }) console.log('Cache configuration:') console.log('- Max size: 1000 entries') console.log('- Active key TTL: 1 hour') console.log('- Historical key TTL: 24 hours') // Simulate some operations that would populate cache console.log('\nšŸ“¦ Keys and tokens are cached automatically') console.log('šŸ“¦ Subsequent requests use cache (faster, less load)') // On logout, clear everything console.log('\n🚪 User logging out...') keyserver.clearCache() console.log('āœ… All keys and service auth tokens cleared from memory') console.log('\nāš ļø Important: Keys are NEVER persisted to disk') console.log('āš ļø This is a security feature') } // ============================================================================ // Example 6: Batch Decryption // ============================================================================ async function example6_batchDecryption() { console.log('\n=== Example 6: Batch Decryption ===\n') const keyserver = new KeyserverClient({ keyserverDid: 'did:web:keyserver.example.com', getServiceAuthToken: async () => 'mock_token', }) // Simulated encrypted posts from feed const encryptedPosts = [ { uri: 'at://did:plc:abc/app.bsky.feed.post/1', author: 'did:plc:abc', encrypted_content: 'cipher1...', key_version: 1, visibility: 'followers', }, { uri: 'at://did:plc:xyz/app.bsky.feed.post/2', author: 'did:plc:xyz', encrypted_content: 'cipher2...', key_version: 2, visibility: 'premium', }, ] console.log(`Decrypting ${encryptedPosts.length} posts in parallel...`) // Decrypt all posts concurrently const results = await Promise.allSettled( encryptedPosts.map(async (post) => { const groupId = `${post.author}#${post.visibility}` const plaintext = await keyserver.decrypt( post.uri, groupId, post.encrypted_content, post.key_version, ) return { uri: post.uri, content: JSON.parse(plaintext), } }), ) // Process results const successful = results.filter((r) => r.status === 'fulfilled') const failed = results.filter((r) => r.status === 'rejected') console.log(`āœ… Successfully decrypted: ${successful.length}`) console.log(`āŒ Failed to decrypt: ${failed.length}`) // In real usage, display successful posts and handle failures gracefully failed.forEach((result, index) => { const reason = (result as PromiseRejectedResult).reason console.log( ` Post ${index + 1}: ${reason instanceof Error ? reason.message : 'Unknown error'}`, ) }) keyserver.clearCache() } // ============================================================================ // Run All Examples // ============================================================================ async function runAllExamples() { console.log('╔═══════════════════════════════════════════════════════════╗') console.log('ā•‘ @atpkeyserver/client - Basic Usage Examples ā•‘') console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•') try { example1_cryptoOnly() await example2_keyserverEncryption() await example3_keyserverDecryption() await example4_errorHandling() await example5_cacheManagement() await example6_batchDecryption() console.log( '\n╔═══════════════════════════════════════════════════════════╗', ) console.log('ā•‘ All examples completed! ā•‘') console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•') console.log('\nšŸ“– For more information:') console.log(' - README.md: Package documentation') console.log(' - docs/ENCRYPTION_PROTOCOL.md: Protocol specification') console.log(' - docs/SECURITY.md: Security best practices') } catch (error) { console.error('\nāŒ Example failed:', error) process.exit(1) } } // Run if executed directly if (import.meta.main) { runAllExamples() }