aboutsummaryrefslogtreecommitdiff
path: root/internal/api/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/server.go')
-rw-r--r--internal/api/server.go291
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