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.go251
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)
}