1package identity
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net"
9 "net/http"
10 "strings"
11
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 "github.com/haileyok/cocoon/plc"
14)
15
16func ResolveHandle(ctx context.Context, handle string) (string, error) {
17 var did string
18
19 _, err := syntax.ParseHandle(handle)
20 if err != nil {
21 return "", err
22 }
23
24 recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
25 if err == nil {
26 for _, rec := range recs {
27 if strings.HasPrefix(rec, "did=") {
28 did = strings.Split(rec, "did=")[1]
29 break
30 }
31 }
32 } else {
33 fmt.Printf("erorr getting txt records: %v\n", err)
34 }
35
36 if did == "" {
37 req, err := http.NewRequestWithContext(
38 ctx,
39 "GET",
40 fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
41 nil,
42 )
43 if err != nil {
44 return "", nil
45 }
46
47 resp, err := http.DefaultClient.Do(req)
48 if err != nil {
49 return "", nil
50 }
51 defer resp.Body.Close()
52
53 if resp.StatusCode != http.StatusOK {
54 io.Copy(io.Discard, resp.Body)
55 return "", fmt.Errorf("unable to resolve handle")
56 }
57
58 b, err := io.ReadAll(resp.Body)
59 if err != nil {
60 return "", err
61 }
62
63 maybeDid := string(b)
64
65 if _, err := syntax.ParseDID(maybeDid); err != nil {
66 return "", fmt.Errorf("unable to resolve handle")
67 }
68
69 did = maybeDid
70 }
71
72 return did, nil
73}
74
75type DidDoc struct {
76 Context []string `json:"@context"`
77 Id string `json:"id"`
78 AlsoKnownAs []string `json:"alsoKnownAs"`
79 VerificationMethods []DidDocVerificationMethod `json:"verificationMethods"`
80 Service []DidDocService `json:"service"`
81}
82
83type DidDocVerificationMethod struct {
84 Id string `json:"id"`
85 Type string `json:"type"`
86 Controller string `json:"controller"`
87 PublicKeyMultibase string `json:"publicKeyMultibase"`
88}
89
90type DidDocService struct {
91 Id string `json:"id"`
92 Type string `json:"type"`
93 ServiceEndpoint string `json:"serviceEndpoint"`
94}
95
96type DidData struct {
97 Did string `json:"did"`
98 VerificationMethods map[string]string `json:"verificationMethods"`
99 RotationKeys []string `json:"rotationKeys"`
100 AlsoKnownAs []string `json:"alsoKnownAs"`
101 Services map[string]DidDataService `json:"services"`
102}
103
104type DidDataService struct {
105 Type string `json:"type"`
106 Endpoint string `json:"endpoint"`
107}
108
109type DidLog []DidLogEntry
110
111type DidLogEntry struct {
112 Sig string `json:"sig"`
113 Prev *string `json:"prev"`
114 Type string `json:"string"`
115 Services map[string]plc.OperationService `json:"services"`
116 AlsoKnownAs []string `json:"alsoKnownAs"`
117 RotationKeys []string `json:"rotationKeys"`
118 VerificationMethods map[string]string `json:"verificationMethods"`
119}
120
121type DidAuditEntry struct {
122 Did string `json:"did"`
123 Operation DidLogEntry `json:"operation"`
124 Cid string `json:"cid"`
125 Nullified bool `json:"nullified"`
126 CreatedAt string `json:"createdAt"`
127}
128
129type DidAuditLog []DidAuditEntry
130
131func FetchDidDoc(ctx context.Context, did string) (*DidDoc, error) {
132 var ustr string
133 if strings.HasPrefix(did, "did:plc:") {
134 ustr = fmt.Sprintf("https://plc.directory/%s", did)
135 } else if strings.HasPrefix(did, "did:web:") {
136 ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:"))
137 } else {
138 return nil, fmt.Errorf("did was not a supported did type")
139 }
140
141 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
142 if err != nil {
143 return nil, err
144 }
145
146 resp, err := http.DefaultClient.Do(req)
147 if err != nil {
148 return nil, err
149 }
150 defer resp.Body.Close()
151
152 if resp.StatusCode != 200 {
153 io.Copy(io.Discard, resp.Body)
154 return nil, fmt.Errorf("could not find identity in plc registry")
155 }
156
157 var diddoc DidDoc
158 if err := json.NewDecoder(resp.Body).Decode(&diddoc); err != nil {
159 return nil, err
160 }
161
162 return &diddoc, nil
163}
164
165func FetchDidData(ctx context.Context, did string) (*DidData, error) {
166 var ustr string
167 ustr = fmt.Sprintf("https://plc.directory/%s/data", did)
168
169 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
170 if err != nil {
171 return nil, err
172 }
173
174 resp, err := http.DefaultClient.Do(req)
175 if err != nil {
176 return nil, err
177 }
178 defer resp.Body.Close()
179
180 if resp.StatusCode != 200 {
181 io.Copy(io.Discard, resp.Body)
182 return nil, fmt.Errorf("could not find identity in plc registry")
183 }
184
185 var diddata DidData
186 if err := json.NewDecoder(resp.Body).Decode(&diddata); err != nil {
187 return nil, err
188 }
189
190 return &diddata, nil
191}
192
193func FetchDidAuditLog(ctx context.Context, did string) (DidAuditLog, error) {
194 var ustr string
195 ustr = fmt.Sprintf("https://plc.directory/%s/log/audit", did)
196
197 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
198 if err != nil {
199 return nil, err
200 }
201
202 resp, err := http.DefaultClient.Do(req)
203 if err != nil {
204 return nil, err
205 }
206 defer resp.Body.Close()
207
208 if resp.StatusCode != 200 {
209 io.Copy(io.Discard, resp.Body)
210 return nil, fmt.Errorf("could not find identity in plc registry")
211 }
212
213 var didlog DidAuditLog
214 if err := json.NewDecoder(resp.Body).Decode(&didlog); err != nil {
215 return nil, err
216 }
217
218 return didlog, nil
219}
220
221func ResolveService(ctx context.Context, did string) (string, error) {
222 diddoc, err := FetchDidDoc(ctx, did)
223 if err != nil {
224 return "", err
225 }
226
227 var service string
228 for _, svc := range diddoc.Service {
229 if svc.Id == "#atproto_pds" {
230 service = svc.ServiceEndpoint
231 }
232 }
233
234 if service == "" {
235 return "", fmt.Errorf("could not find atproto_pds service in identity services")
236 }
237
238 return service, nil
239}