diff options
Diffstat (limited to 'internal/api/server.go')
| -rw-r--r-- | internal/api/server.go | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/internal/api/server.go b/internal/api/server.go index f1ffc7c..f773145 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -65,6 +65,8 @@ func (s *Server) Handler() http.Handler { mux.Handle("/checkpoints/latest", s.withTimeout(http.HandlerFunc(s.handleLatestCheckpoint))) mux.Handle("/checkpoints/", s.withTimeout(http.HandlerFunc(s.handleCheckpointBySequence))) mux.Handle("/did/", s.withTimeout(http.HandlerFunc(s.handleDID))) + mux.Handle("/export", s.withTimeout(http.HandlerFunc(s.handleExportCompatibility))) + mux.Handle("/", s.withTimeout(http.HandlerFunc(s.handlePLCCompatibility))) return mux } @@ -275,6 +277,248 @@ func (s *Server) handleDIDProof(w http.ResponseWriter, r *http.Request, did stri }) } +type plcAuditEntry struct { + DID string `json:"did"` + Operation json.RawMessage `json:"operation"` + CID string `json:"cid"` + Nullified bool `json:"nullified"` + CreatedAt string `json:"createdAt"` +} + +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")) + return + } + if path == "export" { + s.handleExportCompatibility(w, r) + return + } + parts := strings.Split(path, "/") + did := parts[0] + if !strings.HasPrefix(did, "did:") { + writeErr(w, http.StatusNotFound, fmt.Errorf("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")) + return + } + if r.Method != http.MethodGet { + w.Header().Set("Allow", http.MethodGet) + writeErr(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed")) + return + } + + switch { + case len(parts) == 1: + s.handleGetDIDCompatibility(w, did) + case len(parts) == 2 && parts[1] == "data": + s.handleGetDIDDataCompatibility(w, r, did) + case len(parts) == 2 && parts[1] == "log": + s.handleGetDIDLogCompatibility(w, r, did) + case len(parts) == 3 && parts[1] == "log" && parts[2] == "last": + s.handleGetDIDLogLastCompatibility(w, r, did) + case len(parts) == 3 && parts[1] == "log" && parts[2] == "audit": + s.handleGetDIDLogAuditCompatibility(w, r, did) + default: + writeErr(w, http.StatusNotFound, fmt.Errorf("not found")) + } +} + +func (s *Server) handleGetDIDCompatibility(w http.ResponseWriter, did string) { + state, ok, err := s.store.GetState(did) + if err != nil { + writeErr(w, http.StatusInternalServerError, err) + return + } + if !ok { + writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + return + } + status := http.StatusOK + if isTombstonedDIDDocument(state.DIDDocument) { + status = http.StatusGone + } + w.Header().Set("Content-Type", "application/did+ld+json") + w.WriteHeader(status) + _, _ = w.Write(state.DIDDocument) +} + +func (s *Server) handleGetDIDLogCompatibility(w http.ResponseWriter, r *http.Request, did string) { + if s.ingestor == nil { + writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + return + } + logEntries, err := s.ingestor.LoadDIDLog(r.Context(), did) + if err != nil { + if errors.Is(err, ingest.ErrDIDNotFound) || errors.Is(err, ingest.ErrHistoryNotStored) { + writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + return + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + writeErr(w, http.StatusGatewayTimeout, err) + return + } + writeErr(w, http.StatusInternalServerError, err) + return + } + ops := make([]json.RawMessage, 0, len(logEntries)) + for _, rec := range logEntries { + ops = append(ops, rec.Operation) + } + writeJSONWithContentType(w, http.StatusOK, "application/json", ops) +} + +func (s *Server) handleGetDIDLogLastCompatibility(w http.ResponseWriter, r *http.Request, did string) { + if s.ingestor == nil { + writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + return + } + rec, err := s.ingestor.LoadLatestDIDOperation(r.Context(), did) + if err != nil { + if errors.Is(err, ingest.ErrDIDNotFound) || errors.Is(err, ingest.ErrHistoryNotStored) { + writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + return + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + writeErr(w, http.StatusGatewayTimeout, err) + return + } + writeErr(w, http.StatusInternalServerError, err) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(rec.Operation) +} + +func (s *Server) handleGetDIDLogAuditCompatibility(w http.ResponseWriter, r *http.Request, did string) { + if s.ingestor == nil { + writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + return + } + logEntries, err := s.ingestor.LoadDIDLog(r.Context(), did) + if err != nil { + if errors.Is(err, ingest.ErrDIDNotFound) || errors.Is(err, ingest.ErrHistoryNotStored) { + writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + return + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + writeErr(w, http.StatusGatewayTimeout, err) + return + } + writeErr(w, http.StatusInternalServerError, err) + return + } + audit := make([]plcAuditEntry, 0, len(logEntries)) + for _, rec := range logEntries { + audit = append(audit, plcAuditEntry{ + DID: did, + Operation: rec.Operation, + CID: rec.CID, + Nullified: rec.Nullified, + CreatedAt: rec.CreatedAt, + }) + } + writeJSONWithContentType(w, http.StatusOK, "application/json", audit) +} + +func (s *Server) handleGetDIDDataCompatibility(w http.ResponseWriter, r *http.Request, did string) { + if s.ingestor == nil { + writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + return + } + data, err := s.ingestor.LoadCurrentPLCData(r.Context(), did) + if err != nil { + if errors.Is(err, ingest.ErrDIDNotFound) { + writeErr(w, http.StatusNotFound, fmt.Errorf("did not found")) + return + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + writeErr(w, http.StatusGatewayTimeout, err) + return + } + writeErr(w, http.StatusInternalServerError, err) + return + } + writeJSONWithContentType(w, http.StatusOK, "application/json", data) +} + +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")) + return + } + if s.ingestor == nil { + writeErr(w, http.StatusServiceUnavailable, fmt.Errorf("ingestor unavailable")) + return + } + + count := 1000 + if rawCount := strings.TrimSpace(r.URL.Query().Get("count")); rawCount != "" { + n, err := strconv.Atoi(rawCount) + if err != nil || n < 1 { + writeErr(w, http.StatusBadRequest, fmt.Errorf("invalid count query parameter")) + return + } + if n > 1000 { + n = 1000 + } + count = n + } + + var after time.Time + if rawAfter := strings.TrimSpace(r.URL.Query().Get("after")); rawAfter != "" { + parsed, err := time.Parse(time.RFC3339, rawAfter) + if err != nil { + writeErr(w, http.StatusBadRequest, fmt.Errorf("invalid after query parameter")) + return + } + after = parsed + } + + w.Header().Set("Content-Type", "application/jsonlines") + w.WriteHeader(http.StatusOK) + flusher, _ := w.(http.Flusher) + enc := json.NewEncoder(w) + err := s.ingestor.StreamExport(r.Context(), after, count, func(rec types.ExportRecord) error { + entry := plcAuditEntry{ + DID: rec.DID, + Operation: rec.Operation, + CID: rec.CID, + Nullified: rec.Nullified, + CreatedAt: rec.CreatedAt, + } + if err := enc.Encode(entry); err != nil { + return err + } + if flusher != nil { + flusher.Flush() + } + return nil + }) + if err != nil { + // Response has already started; best effort termination. + return + } +} + +func isTombstonedDIDDocument(raw []byte) bool { + if len(raw) == 0 { + return false + } + var doc map[string]any + if err := json.Unmarshal(raw, &doc); err != nil { + return false + } + deactivated, _ := doc["deactivated"].(bool) + return deactivated +} + func (s *Server) withTimeout(next http.Handler) http.Handler { timeout := s.cfg.RequestTimeout if timeout <= 0 { @@ -343,7 +587,14 @@ func (s *Server) selectCheckpointForProof(r *http.Request) (types.CheckpointV1, } func writeJSON(w http.ResponseWriter, code int, v any) { + writeJSONWithContentType(w, code, "application/json", v) +} + +func writeJSONWithContentType(w http.ResponseWriter, code int, contentType string, v any) { w.Header().Set("Content-Type", "application/json") + if strings.TrimSpace(contentType) != "" { + w.Header().Set("Content-Type", contentType) + } w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } |