diff options
Diffstat (limited to 'internal/api/server.go')
| -rw-r--r-- | internal/api/server.go | 291 |
1 files changed, 257 insertions, 34 deletions
diff --git a/internal/api/server.go b/internal/api/server.go index a3ef211..e9e1e15 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -14,6 +14,7 @@ import ( "github.com/Fuwn/plutia/internal/types" "github.com/Fuwn/plutia/pkg/proof" "net/http" + "sort" "strconv" "strings" "time" @@ -364,11 +365,39 @@ type plcAuditEntry struct { CreatedAt string `json:"createdAt"` } +type plcDIDDocument struct { + Context []string `json:"@context"` + ID string `json:"id"` + AlsoKnownAs []string `json:"alsoKnownAs,omitempty"` + VerificationMethod []plcVerificationEntry `json:"verificationMethod,omitempty"` + Service []plcServiceEntry `json:"service,omitempty"` + Deactivated bool `json:"deactivated,omitempty"` +} + +type plcVerificationEntry struct { + ID string `json:"id"` + Type string `json:"type"` + Controller string `json:"controller"` + PublicKeyMultibase string `json:"publicKeyMultibase"` +} + +type plcServiceEntry struct { + ID string `json:"id"` + Type string `json:"type"` + ServiceEndpoint string `json:"serviceEndpoint"` +} + +var plcDIDContexts = []string{ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1", +} + func (s *Server) handlePLCCompatibility(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/") if path == "" { - writeErr(w, http.StatusNotFound, fmt.Errorf("not found")) + writeCompatibilityErr(w, http.StatusNotFound, "not found") return } @@ -383,28 +412,28 @@ func (s *Server) handlePLCCompatibility(w http.ResponseWriter, r *http.Request) did := parts[0] if !strings.HasPrefix(did, "did:") { - writeErr(w, http.StatusNotFound, fmt.Errorf("not found")) + writeCompatibilityErr(w, http.StatusNotFound, "not found") return } if r.Method == http.MethodPost && len(parts) == 1 { w.Header().Set("Allow", http.MethodGet) - writeErr(w, http.StatusMethodNotAllowed, fmt.Errorf("write operations are not supported by this mirror")) + writeCompatibilityErr(w, http.StatusMethodNotAllowed, "write operations are not supported by this mirror") return } if r.Method != http.MethodGet { w.Header().Set("Allow", http.MethodGet) - writeErr(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) + writeCompatibilityErr(w, http.StatusMethodNotAllowed, "method not allowed") return } switch { case len(parts) == 1: - s.handleGetDIDCompatibility(w, did) + s.handleGetDIDCompatibility(w, r, did) case len(parts) == 2 && parts[1] == "data": s.handleGetDIDDataCompatibility(w, r, did) case len(parts) == 2 && parts[1] == "log": @@ -414,40 +443,63 @@ func (s *Server) handlePLCCompatibility(w http.ResponseWriter, r *http.Request) case len(parts) == 3 && parts[1] == "log" && parts[2] == "audit": s.handleGetDIDLogAuditCompatibility(w, r, did) default: - writeErr(w, http.StatusNotFound, fmt.Errorf("not found")) + writeCompatibilityErr(w, http.StatusNotFound, "not found") } } -func (s *Server) handleGetDIDCompatibility(w http.ResponseWriter, did string) { +func (s *Server) handleGetDIDCompatibility(w http.ResponseWriter, r *http.Request, did string) { state, ok, err := s.store.GetState(did) if err != nil { - writeErr(w, http.StatusInternalServerError, err) + writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) return } if !ok { - writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) + + return + } + + if s.ingestor == nil { + writeCompatibilityErr(w, http.StatusServiceUnavailable, "ingestor unavailable") + + return + } + + data, err := s.ingestor.LoadCurrentPLCData(r.Context(), did) + if err != nil { + if errors.Is(err, ingest.ErrDIDNotFound) { + writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) + + return + } + + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + writeCompatibilityErr(w, http.StatusGatewayTimeout, err.Error()) + + return + } + + writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) return } status := http.StatusOK - if isTombstonedDIDDocument(state.DIDDocument) { + deactivated := isTombstonedDIDDocument(state.DIDDocument) + if deactivated { status = http.StatusGone } - w.Header().Set("Content-Type", "application/did+ld+json") - w.WriteHeader(status) - - _, _ = w.Write(state.DIDDocument) + writeJSONWithContentType(w, status, "application/did+ld+json", buildPLCDIDDocument(did, data, deactivated)) } func (s *Server) handleGetDIDLogCompatibility(w http.ResponseWriter, r *http.Request, did string) { if s.ingestor == nil { - writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + writeCompatibilityErr(w, http.StatusServiceUnavailable, "ingestor unavailable") return } @@ -456,18 +508,18 @@ func (s *Server) handleGetDIDLogCompatibility(w http.ResponseWriter, r *http.Req if err != nil { if errors.Is(err, ingest.ErrDIDNotFound) || errors.Is(err, ingest.ErrHistoryNotStored) { - writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) return } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - writeErr(w, http.StatusGatewayTimeout, err) + writeCompatibilityErr(w, http.StatusGatewayTimeout, err.Error()) return } - writeErr(w, http.StatusInternalServerError, err) + writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) return } @@ -483,7 +535,7 @@ func (s *Server) handleGetDIDLogCompatibility(w http.ResponseWriter, r *http.Req func (s *Server) handleGetDIDLogLastCompatibility(w http.ResponseWriter, r *http.Request, did string) { if s.ingestor == nil { - writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + writeCompatibilityErr(w, http.StatusServiceUnavailable, "ingestor unavailable") return } @@ -492,18 +544,18 @@ func (s *Server) handleGetDIDLogLastCompatibility(w http.ResponseWriter, r *http if err != nil { if errors.Is(err, ingest.ErrDIDNotFound) || errors.Is(err, ingest.ErrHistoryNotStored) { - writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) return } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - writeErr(w, http.StatusGatewayTimeout, err) + writeCompatibilityErr(w, http.StatusGatewayTimeout, err.Error()) return } - writeErr(w, http.StatusInternalServerError, err) + writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) return } @@ -516,7 +568,7 @@ func (s *Server) handleGetDIDLogLastCompatibility(w http.ResponseWriter, r *http func (s *Server) handleGetDIDLogAuditCompatibility(w http.ResponseWriter, r *http.Request, did string) { if s.ingestor == nil { - writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + writeCompatibilityErr(w, http.StatusServiceUnavailable, "ingestor unavailable") return } @@ -525,18 +577,18 @@ func (s *Server) handleGetDIDLogAuditCompatibility(w http.ResponseWriter, r *htt if err != nil { if errors.Is(err, ingest.ErrDIDNotFound) || errors.Is(err, ingest.ErrHistoryNotStored) { - writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) return } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - writeErr(w, http.StatusGatewayTimeout, err) + writeCompatibilityErr(w, http.StatusGatewayTimeout, err.Error()) return } - writeErr(w, http.StatusInternalServerError, err) + writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) return } @@ -558,7 +610,7 @@ func (s *Server) handleGetDIDLogAuditCompatibility(w http.ResponseWriter, r *htt func (s *Server) handleGetDIDDataCompatibility(w http.ResponseWriter, r *http.Request, did string) { if s.ingestor == nil { - writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + writeCompatibilityErr(w, http.StatusServiceUnavailable, "ingestor unavailable") return } @@ -567,18 +619,18 @@ func (s *Server) handleGetDIDDataCompatibility(w http.ResponseWriter, r *http.Re if err != nil { if errors.Is(err, ingest.ErrDIDNotFound) { - writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) return } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - writeErr(w, http.StatusGatewayTimeout, err) + writeCompatibilityErr(w, http.StatusGatewayTimeout, err.Error()) return } - writeErr(w, http.StatusInternalServerError, err) + writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) return } @@ -589,13 +641,13 @@ func (s *Server) handleGetDIDDataCompatibility(w http.ResponseWriter, r *http.Re func (s *Server) handleExportCompatibility(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.Header().Set("Allow", http.MethodGet) - writeErr(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) + writeCompatibilityErr(w, http.StatusMethodNotAllowed, "method not allowed") return } if s.ingestor == nil { - writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + writeCompatibilityErr(w, http.StatusServiceUnavailable, "ingestor unavailable") return } @@ -606,7 +658,7 @@ func (s *Server) handleExportCompatibility(w http.ResponseWriter, r *http.Reques n, err := strconv.Atoi(rawCount) if err != nil || n < 1 { - writeErr(w, http.StatusBadRequest, fmt.Errorf("invalid count query parameter")) + writeCompatibilityErr(w, http.StatusBadRequest, "invalid count query parameter") return } @@ -624,7 +676,7 @@ func (s *Server) handleExportCompatibility(w http.ResponseWriter, r *http.Reques parsed, err := time.Parse(time.RFC3339, rawAfter) if err != nil { - writeErr(w, http.StatusBadRequest, fmt.Errorf("invalid after query parameter")) + writeCompatibilityErr(w, http.StatusBadRequest, "invalid after query parameter") return } @@ -679,6 +731,177 @@ func isTombstonedDIDDocument(raw []byte) bool { return deactivated } +func buildPLCDIDDocument(did string, plcData map[string]any, deactivated bool) plcDIDDocument { + doc := plcDIDDocument{ + Context: append([]string(nil), plcDIDContexts...), + ID: did, + AlsoKnownAs: extractStringArray(plcData["alsoKnownAs"]), + Deactivated: deactivated, + } + + verificationMethods := extractVerificationMethodMap(plcData["verificationMethods"]) + if len(verificationMethods) > 0 { + names := make([]string, 0, len(verificationMethods)) + for name := range verificationMethods { + names = append(names, name) + } + + sort.Strings(names) + + doc.VerificationMethod = make([]plcVerificationEntry, 0, len(names)) + for _, name := range names { + value := verificationMethods[name] + if strings.TrimSpace(value) == "" { + continue + } + + doc.VerificationMethod = append(doc.VerificationMethod, plcVerificationEntry{ + ID: did + "#" + name, + Type: "Multikey", + Controller: did, + PublicKeyMultibase: value, + }) + } + } + + services := extractServicesMap(plcData["services"]) + if len(services) > 0 { + names := make([]string, 0, len(services)) + for name := range services { + names = append(names, name) + } + + sort.Strings(names) + + doc.Service = make([]plcServiceEntry, 0, len(names)) + for _, name := range names { + entry := services[name] + typ := entry["type"] + endpoint := entry["endpoint"] + + if strings.TrimSpace(endpoint) == "" { + continue + } + + doc.Service = append(doc.Service, plcServiceEntry{ + ID: "#" + name, + Type: typ, + ServiceEndpoint: endpoint, + }) + } + } + + return doc +} + +func extractStringArray(v any) []string { + switch raw := v.(type) { + case []string: + out := make([]string, 0, len(raw)) + for _, item := range raw { + item = strings.TrimSpace(item) + if item == "" { + continue + } + + out = append(out, item) + } + + return out + case []any: + out := make([]string, 0, len(raw)) + for _, item := range raw { + s, _ := item.(string) + if strings.TrimSpace(s) == "" { + continue + } + + out = append(out, s) + } + + return out + default: + return nil + } +} + +func extractVerificationMethodMap(v any) map[string]string { + out := map[string]string{} + + switch vm := v.(type) { + case map[string]string: + for name, key := range vm { + if strings.TrimSpace(key) == "" { + continue + } + + out[name] = key + } + case map[string]any: + for name, raw := range vm { + key, _ := raw.(string) + if strings.TrimSpace(key) == "" { + continue + } + + out[name] = key + } + } + + return out +} + +func extractServicesMap(v any) map[string]map[string]string { + out := map[string]map[string]string{} + + switch services := v.(type) { + case map[string]map[string]string: + for name, entry := range services { + endpoint := strings.TrimSpace(entry["endpoint"]) + if endpoint == "" { + endpoint = strings.TrimSpace(entry["serviceEndpoint"]) + } + + if endpoint == "" { + continue + } + + out[name] = map[string]string{ + "type": entry["type"], + "endpoint": endpoint, + } + } + case map[string]any: + for name, raw := range services { + entry, ok := raw.(map[string]any) + if !ok { + continue + } + + typ, _ := entry["type"].(string) + endpoint, _ := entry["endpoint"].(string) + if endpoint == "" { + endpoint, _ = entry["serviceEndpoint"].(string) + } + + if strings.TrimSpace(endpoint) == "" { + continue + } + + out[name] = map[string]string{ + "type": typ, + "endpoint": endpoint, + } + } + } + + return out +} + +func writeCompatibilityErr(w http.ResponseWriter, code int, message string) { + writeJSON(w, code, map[string]any{"message": message}) +} + func (s *Server) withTimeout(next http.Handler) http.Handler { timeout := s.cfg.RequestTimeout |