1import 'dart:convert'; 2 3/// Coves Session Model 4/// 5/// Simplified session model for the backend OAuth flow. 6/// The backend handles all the complexity (DPoP, PKCE, token refresh) 7/// and gives us a sealed token that's opaque to the client. 8/// 9/// This replaces the complex TokenSet + DPoP keys from atproto_oauth_flutter. 10class CovesSession { 11 const CovesSession({ 12 required this.token, 13 required this.did, 14 required this.sessionId, 15 this.handle, 16 }); 17 18 /// Create a session from OAuth callback parameters 19 /// 20 /// Expected URL format (RFC 8252 private-use URI scheme): 21 /// `social.coves:/callback?token=...&did=...&session_id=...&handle=...` 22 factory CovesSession.fromCallbackUri(Uri uri) { 23 final token = uri.queryParameters['token']; 24 final did = uri.queryParameters['did']; 25 final sessionId = uri.queryParameters['session_id']; 26 final handle = uri.queryParameters['handle']; 27 28 if (token == null || token.isEmpty) { 29 throw const FormatException('Missing required parameter: token'); 30 } 31 if (did == null || did.isEmpty) { 32 throw const FormatException('Missing required parameter: did'); 33 } 34 if (sessionId == null || sessionId.isEmpty) { 35 throw const FormatException('Missing required parameter: session_id'); 36 } 37 38 return CovesSession( 39 token: Uri.decodeComponent(token), 40 did: did, 41 sessionId: sessionId, 42 handle: handle, 43 ); 44 } 45 46 /// Create a session from JSON (for storage restoration) 47 factory CovesSession.fromJson(Map<String, dynamic> json) { 48 return CovesSession( 49 token: json['token'] as String, 50 did: json['did'] as String, 51 sessionId: json['session_id'] as String, 52 handle: json['handle'] as String?, 53 ); 54 } 55 56 /// Create a session from a JSON string 57 factory CovesSession.fromJsonString(String jsonString) { 58 return CovesSession.fromJson( 59 jsonDecode(jsonString) as Map<String, dynamic>, 60 ); 61 } 62 63 /// The sealed session token (AES-256-GCM encrypted by backend) 64 /// 65 /// This token is opaque to the client - we just store and send it. 66 /// Use in Authorization header: `Authorization: Bearer $token` 67 final String token; 68 69 /// User's DID (decentralized identifier) 70 /// 71 /// Example: did:plc:abc123 72 final String did; 73 74 /// Session ID for refresh operations 75 /// 76 /// The backend uses this to identify the session for token refresh. 77 final String sessionId; 78 79 /// User's handle (optional) 80 /// 81 /// Example: alice.bsky.social 82 /// May be null if the backend didn't include it in the callback. 83 final String? handle; 84 85 /// Convert to JSON for storage 86 Map<String, dynamic> toJson() { 87 return { 88 'token': token, 89 'did': did, 90 'session_id': sessionId, 91 if (handle != null) 'handle': handle, 92 }; 93 } 94 95 /// Convert to JSON string for storage 96 String toJsonString() => jsonEncode(toJson()); 97 98 /// Create a copy with updated token (for refresh) 99 CovesSession copyWithToken(String newToken) { 100 return CovesSession( 101 token: newToken, 102 did: did, 103 sessionId: sessionId, 104 handle: handle, 105 ); 106 } 107 108 @override 109 String toString() { 110 return 'CovesSession(did: $did, handle: $handle, sessionId: $sessionId)'; 111 } 112}