Main coves client
1import 'constants.dart';
2import 'handle_helpers.dart';
3
4/// Represents a DID document as defined by W3C DID Core spec.
5///
6/// This is a simplified version focused on atProto needs.
7/// See: https://www.w3.org/TR/did-core/
8class DidDocument {
9 /// The DID subject (the DID itself)
10 final String id;
11
12 /// Alternative identifiers (used for atProto handles: at://handle)
13 final List<String>? alsoKnownAs;
14
15 /// Service endpoints (used to find PDS URL)
16 final List<DidService>? service;
17
18 /// Verification methods for authentication
19 final List<dynamic>? verificationMethod;
20
21 /// Authentication methods
22 final List<dynamic>? authentication;
23
24 /// Optional controller DIDs
25 final dynamic controller; // Can be String or List<String>
26
27 /// The @context field
28 final dynamic context;
29
30 const DidDocument({
31 required this.id,
32 this.alsoKnownAs,
33 this.service,
34 this.verificationMethod,
35 this.authentication,
36 this.controller,
37 this.context,
38 });
39
40 /// Parses a DID document from JSON.
41 factory DidDocument.fromJson(Map<String, dynamic> json) {
42 return DidDocument(
43 id: json['id'] as String,
44 alsoKnownAs:
45 (json['alsoKnownAs'] as List<dynamic>?)
46 ?.map((e) => e as String)
47 .toList(),
48 service:
49 (json['service'] as List<dynamic>?)
50 ?.map((e) => DidService.fromJson(e as Map<String, dynamic>))
51 .toList(),
52 verificationMethod: json['verificationMethod'] as List<dynamic>?,
53 authentication: json['authentication'] as List<dynamic>?,
54 controller: json['controller'],
55 context: json['@context'],
56 );
57 }
58
59 /// Converts the DID document to JSON.
60 Map<String, dynamic> toJson() {
61 final map = <String, dynamic>{'id': id};
62
63 if (context != null) map['@context'] = context;
64 if (alsoKnownAs != null) map['alsoKnownAs'] = alsoKnownAs;
65 if (service != null) {
66 map['service'] = service!.map((s) => s.toJson()).toList();
67 }
68 if (verificationMethod != null) {
69 map['verificationMethod'] = verificationMethod;
70 }
71 if (authentication != null) map['authentication'] = authentication;
72 if (controller != null) map['controller'] = controller;
73
74 return map;
75 }
76
77 /// Extracts the atProto PDS URL from the DID document.
78 ///
79 /// Returns null if no PDS service is found.
80 String? extractPdsUrl() {
81 if (service == null) return null;
82
83 for (final s in service!) {
84 // Check for standard atproto_pds service
85 if (s.id == atprotoServiceId && s.type == atprotoServiceType) {
86 if (s.serviceEndpoint is String) {
87 return s.serviceEndpoint as String;
88 }
89 }
90
91 // Also check if type matches (some implementations may vary on id)
92 if (s.type == atprotoServiceType && s.serviceEndpoint is String) {
93 return s.serviceEndpoint as String;
94 }
95 }
96
97 return null;
98 }
99
100 /// Extracts the raw atProto handle from the DID document.
101 ///
102 /// Returns null if no handle is found in alsoKnownAs.
103 String? extractAtprotoHandle() {
104 if (alsoKnownAs == null) return null;
105
106 for (final aka in alsoKnownAs!) {
107 if (aka.startsWith('at://')) {
108 // Strip off "at://" prefix
109 return aka.substring(5);
110 }
111 }
112
113 return null;
114 }
115
116 /// Extracts a validated, normalized atProto handle from the DID document.
117 ///
118 /// Returns null if no valid handle is found.
119 String? extractNormalizedHandle() {
120 final handle = extractAtprotoHandle();
121 if (handle == null) return null;
122 return asNormalizedHandle(handle);
123 }
124}
125
126/// Represents a service endpoint in a DID document.
127class DidService {
128 /// Service ID (e.g., "#atproto_pds")
129 final String id;
130
131 /// Service type (e.g., "AtprotoPersonalDataServer")
132 final String type;
133
134 /// Service endpoint URL
135 final dynamic serviceEndpoint; // Can be String, Map, or List
136
137 const DidService({
138 required this.id,
139 required this.type,
140 required this.serviceEndpoint,
141 });
142
143 /// Parses a service from JSON.
144 factory DidService.fromJson(Map<String, dynamic> json) {
145 return DidService(
146 id: json['id'] as String,
147 type: json['type'] as String,
148 serviceEndpoint: json['serviceEndpoint'],
149 );
150 }
151
152 /// Converts the service to JSON.
153 Map<String, dynamic> toJson() {
154 return {'id': id, 'type': type, 'serviceEndpoint': serviceEndpoint};
155 }
156}