Main coves client
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}