1package api 2 3import ( 4 "encoding/json" 5 "fmt" 6 "log/slog" 7 "net/http" 8 "reflect" 9 "regexp" 10 "slices" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/go-chi/chi/v5" 16 "github.com/go-chi/chi/v5/middleware" 17 _ "github.com/lib/pq" 18 did "github.com/whyrusleeping/go-did" 19 20 "tangled.sh/seiso.moe/alethia.directory/ent" 21 "tangled.sh/seiso.moe/alethia.directory/ent/operation" 22 "tangled.sh/seiso.moe/alethia.directory/ent/syncstatus" 23 "tangled.sh/seiso.moe/alethia.directory/pkg/plc" 24) 25 26var ( 27 validDIDPattern = regexp.MustCompile(`[a-z0-9:]{24}`) 28) 29 30type Server struct { 31 client *ent.Client 32 logger *slog.Logger 33 port int 34} 35 36type DidDocService struct { 37 ID string `json:"id"` 38 Type string `json:"type"` 39 ServiceEndpoint string `json:"serviceEndpoint"` 40} 41 42type DidDocument struct { 43 Context []string `json:"@context"` 44 ID string `json:"id"` 45 AlsoKnownAs []string `json:"alsoKnownAs"` 46 VerificationMethod []VerificationMethod `json:"verificationMethod"` 47 Service []DidDocService `json:"service"` 48} 49 50type VerificationMethod struct { 51 ID string 52 Type string 53 Controller string 54 PublicKeyMultibase string 55} 56 57type KeyAndContext struct { 58 Context *string 59 Type string 60 PublicKeyMultiBase string 61} 62 63func NewServer(client *ent.Client, logger *slog.Logger, port int) *Server { 64 return &Server{ 65 client: client, 66 logger: logger, 67 port: port, 68 } 69} 70 71func (s *Server) Start() error { 72 r := chi.NewRouter() 73 r.Use(middleware.Logger) 74 // unofficial endpoints 75 r.Get("/_health", s.handleHealth) 76 77 // official endpoints 78 r.Get("/{did}", s.handleDid) 79 r.Get("/{did}/log", s.handleLog) 80 r.Get("/{did}/log/audit", s.handleAudit) 81 r.Get("/{did}/log/last", s.handleLastOp) 82 r.Get("/{did}/data", s.handlePlcData) 83 r.Get("/export", s.handleExport) 84 85 s.logger.Info("starting API server", "port", s.port) 86 return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r) 87} 88 89func validDID(did string) bool { 90 return validDIDPattern.MatchString(did) 91} 92 93func formatKeyAndContext(key string) KeyAndContext { 94 keyInfo, err := did.PubKeyFromDIDString(key) 95 if err != nil { 96 return KeyAndContext{ 97 Context: nil, 98 Type: "Multikey", 99 PublicKeyMultiBase: strings.ReplaceAll(key, "did:key:", ""), 100 } 101 } 102 103 switch keyInfo.Type { 104 case did.KeyTypeSecp256k1: 105 str := "https://w3id.org/security/suites/secp256k1-2019/v1" 106 return KeyAndContext{ 107 Context: &str, 108 Type: "MultiKey", 109 PublicKeyMultiBase: strings.ReplaceAll(key, "did:key:", ""), 110 } 111 case did.KeyTypeP256: 112 str := "https://w3id.org/security/suites/secp256k1-2019/v1" 113 return KeyAndContext{ 114 Context: &str, 115 Type: "MultiKey", 116 PublicKeyMultiBase: strings.ReplaceAll(key, "did:key:", ""), 117 } 118 default: 119 str := "https://w3id.org/security/suites/ecdsa-2019/v1" 120 return KeyAndContext{ 121 Context: &str, 122 Type: "MultiKey", 123 PublicKeyMultiBase: strings.ReplaceAll(key, "did:key:", ""), 124 } 125 } 126} 127 128func formatDidDoc(did string, operation plc.PLCOperation) DidDocument { 129 context := []string{ 130 "https://www.w3.org/ns/did/v1", 131 "https://w3id.org/security/multikey/v1", 132 } 133 134 verificationMethods := make([]VerificationMethod, 0, len(operation.VerificationMethods)) 135 for k, v := range operation.VerificationMethods { 136 info := formatKeyAndContext(v) 137 if info.Context != nil && !slices.Contains(context, *info.Context) { 138 context = append(context, *info.Context) 139 } 140 verificationMethods = append(verificationMethods, VerificationMethod{ 141 ID: fmt.Sprintf("%s#%s", did, k), 142 Type: info.Type, 143 Controller: did, 144 PublicKeyMultibase: info.PublicKeyMultiBase, 145 }) 146 } 147 148 services := make([]DidDocService, 0, len(operation.Services)) 149 for k, v := range operation.Services { 150 services = append(services, DidDocService{ 151 ID: fmt.Sprintf("#%s", k), 152 Type: v.Type, 153 ServiceEndpoint: v.Endpoint, 154 }) 155 } 156 157 return DidDocument{ 158 Context: context, 159 ID: did, 160 AlsoKnownAs: operation.AlsoKnownAs, 161 VerificationMethod: verificationMethods, 162 Service: services, 163 } 164} 165 166func (s *Server) handleDid(w http.ResponseWriter, r *http.Request) { 167 ctx := r.Context() 168 did := r.PathValue("did") 169 170 if !validDID(did) { 171 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 172 return 173 } 174 175 n, err := s.client.Operation.Query(). 176 Where(operation.Did(did)). 177 Where(operation.NullifiedEQ(false)). 178 Order(ent.Desc(operation.FieldCreatedAt)). 179 First(ctx) 180 181 if err != nil { 182 if ent.IsNotFound(err) { 183 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 184 return 185 } 186 s.logger.Error("database query failed", "error", err) 187 w.WriteHeader(http.StatusInternalServerError) 188 return 189 } 190 191 if n.Operation.GetType() == plc.OperationTypeTombstone { 192 s.writeErrorResponse(w, http.StatusOK, fmt.Sprintf("DID not available: %s", did)) 193 return 194 } 195 196 didDoc := formatDidDoc(did, n.Operation) 197 s.writeJSONResponse(w, http.StatusOK, didDoc) 198} 199 200func (s *Server) handlePlcData(w http.ResponseWriter, r *http.Request) { 201 ctx := r.Context() 202 did := r.PathValue("did") 203 204 if !validDID(did) { 205 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 206 return 207 } 208 209 n, err := s.client.Operation.Query(). 210 Where(operation.Did(did)). 211 Where(operation.NullifiedEQ(false)). 212 Order(ent.Desc(operation.FieldCreatedAt)). 213 First(ctx) 214 215 if err != nil { 216 if ent.IsNotFound(err) { 217 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 218 return 219 } 220 s.logger.Error("database query failed", "error", err) 221 w.WriteHeader(http.StatusInternalServerError) 222 return 223 } 224 225 if n.Operation.GetType() == plc.OperationTypeTombstone { 226 s.writeErrorResponse(w, http.StatusOK, fmt.Sprintf("DID not available: %s", did)) 227 return 228 } 229 230 resp := struct { 231 did string 232 VerificationMethods map[string]string 233 RotationKeys []string 234 AlsoKnownAs []string 235 Services map[string]plc.ServiceEndpoint 236 }{ 237 did: did, 238 VerificationMethods: n.Operation.VerificationMethods, 239 RotationKeys: n.Operation.RotationKeys, 240 AlsoKnownAs: n.Operation.AlsoKnownAs, 241 Services: n.Operation.Services, 242 } 243 s.writeJSONResponse(w, http.StatusOK, resp) 244} 245 246func (s *Server) handleLog(w http.ResponseWriter, r *http.Request) { 247 ctx := r.Context() 248 did := r.PathValue("did") 249 250 if !validDID(did) { 251 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 252 return 253 } 254 255 n, err := s.client.Operation.Query(). 256 Where(operation.Did(did)). 257 Where(operation.NullifiedEQ(false)). 258 Order(ent.Asc(operation.FieldCreatedAt)). 259 All(ctx) 260 261 if err != nil { 262 if ent.IsNotFound(err) { 263 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 264 return 265 } 266 s.logger.Error("database query failed", "error", err) 267 w.WriteHeader(http.StatusInternalServerError) 268 return 269 } 270 271 ops := make([]any, 0, len(n)) 272 for _, v := range n { 273 ops = append(ops, v.Operation) 274 } 275 276 s.writeJSONResponse(w, http.StatusOK, ops) 277} 278 279func (s *Server) handleAudit(w http.ResponseWriter, r *http.Request) { 280 ctx := r.Context() 281 did := r.PathValue("did") 282 283 if !validDID(did) { 284 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 285 return 286 } 287 288 n, err := s.client.Operation.Query(). 289 Where(operation.Did(did)). 290 Where(operation.NullifiedEQ(false)). 291 Order(ent.Asc(operation.FieldCreatedAt)). 292 All(ctx) 293 294 if err != nil { 295 if ent.IsNotFound(err) { 296 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 297 return 298 } 299 s.logger.Error("database query failed", "error", err) 300 w.WriteHeader(http.StatusInternalServerError) 301 return 302 } 303 304 s.writeJSONResponse(w, http.StatusOK, n) 305} 306 307func (s *Server) handleLastOp(w http.ResponseWriter, r *http.Request) { 308 ctx := r.Context() 309 did := r.PathValue("did") 310 311 if !validDID(did) { 312 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 313 return 314 } 315 316 n, err := s.client.Operation.Query(). 317 Where(operation.Did(did)). 318 Where(operation.NullifiedEQ(false)). 319 Order(ent.Desc(operation.FieldCreatedAt)). 320 First(ctx) 321 322 if err != nil { 323 if ent.IsNotFound(err) { 324 s.writeErrorResponse(w, http.StatusNotFound, fmt.Sprintf("DID not registered: %s", did)) 325 return 326 } 327 s.logger.Error("database query failed", "error", err) 328 w.WriteHeader(http.StatusInternalServerError) 329 return 330 } 331 332 s.writeJSONResponse(w, http.StatusOK, n.Operation) 333} 334 335func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { 336 ctx := r.Context() 337 338 countStr := r.FormValue("count") 339 var count int 340 if countStr == "" { 341 count = 10 342 } else { 343 var err error 344 count, err = strconv.Atoi(r.FormValue("count")) 345 if err != nil { 346 s.writeErrorResponse(w, http.StatusBadRequest, "invalid count parameter") 347 return 348 } 349 } 350 count = min(1000, count) 351 352 after, err := time.Parse(time.RFC3339Nano, r.FormValue("after")) 353 if err != nil { 354 after = time.Time{} 355 } 356 357 ops, err := s.client.Operation.Query(). 358 Select( 359 operation.FieldDid, 360 operation.FieldOperation, 361 operation.FieldCid, 362 operation.FieldNullified, 363 operation.FieldCreatedAt, 364 ). 365 Where(operation.CreatedAtGT(after)). 366 Order(ent.Asc(operation.FieldCreatedAt)). 367 Limit(count). 368 All(ctx) 369 if err != nil { 370 s.logger.Error("failed to query operations", "error", err) 371 w.WriteHeader(http.StatusInternalServerError) 372 return 373 } 374 375 s.writeJSONLResponse(w, http.StatusOK, ops) 376} 377 378func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { 379 ctx := r.Context() 380 381 syncStatus, err := s.client.SyncStatus.Query(). 382 Where(syncstatus.Key("last_mirror_sync")). 383 First(ctx) 384 385 if err != nil { 386 if ent.IsNotFound(err) { 387 s.writeJSONResponse(w, http.StatusOK, map[string]any{ 388 "status": "starting", 389 "message": "No sync data available yet", 390 }) 391 return 392 } 393 s.logger.Error("failed to query sync status", "error", err) 394 w.WriteHeader(http.StatusInternalServerError) 395 return 396 } 397 398 s.writeJSONResponse(w, http.StatusOK, map[string]any{ 399 "status": "healthy", 400 "last_updated_at": syncStatus.LastSyncTime.Format(time.RFC3339), 401 }) 402} 403 404func (s *Server) writeErrorResponse(w http.ResponseWriter, statusCode int, message string) { 405 s.writeJSONResponse(w, statusCode, map[string]any{"message": message}) 406} 407 408func (s *Server) writeJSONResponse(w http.ResponseWriter, statusCode int, data any) { 409 w.Header().Set("Content-Type", "application/json") 410 w.WriteHeader(statusCode) 411 412 b, err := json.Marshal(data) 413 if err != nil { 414 s.logger.Error("failed to marshal JSON response", "error", err) 415 return 416 } 417 418 if _, err := w.Write(b); err != nil { 419 s.logger.Error("failed to write response", "error", err) 420 } 421} 422 423func (s *Server) writeJSONLResponse(w http.ResponseWriter, statusCode int, data any) { 424 w.Header().Set("Content-Type", "application/jsonlines") 425 w.WriteHeader(statusCode) 426 427 rv := reflect.ValueOf(data) 428 if rv.Kind() != reflect.Slice { 429 s.logger.Error("JSONL response expects slice data") 430 return 431 } 432 433 for i := range rv.Len() { 434 item := rv.Index(i).Interface() 435 if err := s.writeJSONLine(w, item); err != nil { 436 s.logger.Error("failed to write item as JSON line", "error", err) 437 return 438 } 439 } 440} 441 442func (s *Server) writeJSONLine(w http.ResponseWriter, data any) error { 443 b, err := json.Marshal(data) 444 if err != nil { 445 return err 446 } 447 448 if _, err := w.Write(b); err != nil { 449 return err 450 } 451 if _, err := w.Write([]byte("\n")); err != nil { 452 return err 453 } 454 455 return nil 456}