v2.0.0: claims model - signed MMUR2 votes in a truth DB, merged view in a rebuildable cache DB; POST /api/claims, claims and merge routes, unsigned writes answer 410
Build and Push Docker Image / build (push) Successful in 5m1s

This commit is contained in:
Worph
2026-10-25 17:36:00 +00:00
parent b0adefb97b
commit 84af298792
78 changed files with 7203 additions and 5617 deletions
+1 -317
View File
@@ -4,7 +4,6 @@ import (
"crypto/sha256"
"encoding/base32"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
@@ -89,42 +88,6 @@ const (
ErrDeprecatedField = "deprecated_field"
)
// rejectDeprecatedField reports whether a field name is one of the removed
// per-algorithm CID shapes (`cid_sha2-256`, `cid_midhash256`, `midhash256`, …).
//
// This is a tripwire, not a compatibility shim. Such a field used to be
// accepted and written verbatim — but the reverse-index hook
// (`maybeAddAliasFromFieldLocked`) only registers aliases for fields named
// `cids/<cid>`, so a `cid_*` field was stored and then NEVER INDEXED: the
// record silently became unresolvable by that CID. Failing the write loudly is
// strictly better than a record that looks fine and cannot be found.
//
// Writers must emit the bare-CID key-set instead: `cids/<bareCid>` = "true".
// See METADATA_KEYS.md §2/§14.13.
func rejectDeprecatedField(field string) bool {
return strings.HasPrefix(field, "cid_") ||
field == "midhash256" ||
field == "canonical_cid"
}
// validateFields rejects a write carrying any deprecated CID field. Returns
// true if the request was rejected (the handler must return immediately).
func validateFields(w http.ResponseWriter, metadata map[string]string) bool {
for field := range metadata {
if rejectDeprecatedField(field) {
writeErrorSlug(w, http.StatusBadRequest, ErrDeprecatedField,
fmt.Sprintf("field %q is a removed per-algorithm CID shape; "+
"write the bare-CID key-set instead: \"cids/<bareCid>\": \"true\" "+
"(a cid_* field would be stored but never indexed, leaving the "+
"record unresolvable by that CID). See METADATA_KEYS.md §2/§14.13.",
field),
false)
return true
}
}
return false
}
// defaultSlugFor maps an HTTP status code to the most likely error slug.
// Handlers that want to override (e.g. /api/meta/{cid} → "unknown_cid"
// instead of "unknown_root" on 404) call writeErrorSlug directly.
@@ -250,7 +213,7 @@ func (s *Server) handleGetMeta(w http.ResponseWriter, r *http.Request) {
// the watcher minted the root, regardless of which CID the caller knows.
hashID = s.storage.ResolveRoot(hashID)
metadata, err := s.storage.GetMetadataFlat(hashID)
metadata, err := s.storage.GetMetadataFlatAs(hashID, r.URL.Query().Get("as"))
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
@@ -269,76 +232,6 @@ func (s *Server) handleGetMeta(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, response)
}
// handlePutMeta handles PUT /meta/{hash}
func (s *Server) handlePutMeta(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
hashID := vars["hash"]
if hashID == "" {
writeError(w, http.StatusBadRequest, "hash is required")
return
}
if !s.storage.IsConnected() {
writeError(w, http.StatusServiceUnavailable, "storage not connected")
return
}
// CID → UUID resolution so writes land on the canonical root, not on
// a parallel midhash-rooted entry the watcher's UUID never sees.
hashID = s.storage.ResolveRoot(hashID)
var metadata map[string]string
if err := json.NewDecoder(r.Body).Decode(&metadata); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if validateFields(w, metadata) {
return
}
if err := s.storage.SetMetadataFlat(hashID, metadata); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"hashId": hashID,
})
}
// handleDeleteMeta handles DELETE /meta/{hash}
func (s *Server) handleDeleteMeta(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
hashID := vars["hash"]
if hashID == "" {
writeError(w, http.StatusBadRequest, "hash is required")
return
}
if !s.storage.IsConnected() {
writeError(w, http.StatusServiceUnavailable, "storage not connected")
return
}
hashID = s.storage.ResolveRoot(hashID)
deleted, err := s.storage.DeleteMetadata(hashID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"hashId": hashID,
"deleted": deleted,
})
}
// handleListMeta handles GET /meta
func (s *Server) handleListMeta(w http.ResponseWriter, r *http.Request) {
if !s.storage.IsConnected() {
@@ -623,50 +516,6 @@ func writeErrorSlug(w http.ResponseWriter, status int, slug, message string, ret
})
}
// handlePatchMeta handles PATCH /meta/{hash}
// Merges the provided metadata into existing (does not delete missing keys)
func (s *Server) handlePatchMeta(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
hashID := vars["hash"]
if hashID == "" {
writeError(w, http.StatusBadRequest, "hash is required")
return
}
if !s.storage.IsConnected() {
writeError(w, http.StatusServiceUnavailable, "storage not connected")
return
}
// PATCH is the hot write path for meta-sort. Resolve so plugin output
// merges into the watcher's UUID root rather than minting a parallel
// midhash-rooted entry.
hashID = s.storage.ResolveRoot(hashID)
var metadata map[string]string
if err := json.NewDecoder(r.Body).Decode(&metadata); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if validateFields(w, metadata) {
return
}
updated, err := s.storage.MergeMetadataFlat(hashID, metadata)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"hashId": hashID,
"updated": updated,
})
}
// handleGetProperty handles GET /meta/{hash}/{key}
// Gets a single property value
func (s *Server) handleGetProperty(w http.ResponseWriter, r *http.Request) {
@@ -713,171 +562,6 @@ type PropertyBody struct {
Value string `json:"value"`
}
// handlePutProperty handles PUT /meta/{hash}/{key}
// Sets a single property value
// Accepts JSON body with {"value": "..."} or plain text
func (s *Server) handlePutProperty(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
hashID := vars["hash"]
key := vars["key"]
if hashID == "" {
writeError(w, http.StatusBadRequest, "hash is required")
return
}
if key == "" {
writeError(w, http.StatusBadRequest, "key is required")
return
}
if validateFields(w, map[string]string{key: ""}) {
return
}
if !s.storage.IsConnected() {
writeError(w, http.StatusServiceUnavailable, "storage not connected")
return
}
hashID = s.storage.ResolveRoot(hashID)
// Read body
buf := make([]byte, 1024*1024) // 1MB max
n, err := r.Body.Read(buf)
if err != nil && err.Error() != "EOF" {
writeError(w, http.StatusBadRequest, "failed to read body")
return
}
body := buf[:n]
// Try to parse as JSON {"value": "..."} first
var propBody PropertyBody
var value string
if err := json.Unmarshal(body, &propBody); err == nil && propBody.Value != "" {
// JSON body with value field
value = propBody.Value
} else {
// Plain text body
value = string(body)
}
if err := s.storage.SetProperty(hashID, key, value); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"hashId": hashID,
"property": key,
})
}
// handleDeleteProperty handles DELETE /meta/{hash}/{key}
// Deletes a single property
func (s *Server) handleDeleteProperty(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
hashID := vars["hash"]
key := vars["key"]
if hashID == "" {
writeError(w, http.StatusBadRequest, "hash is required")
return
}
if key == "" {
writeError(w, http.StatusBadRequest, "key is required")
return
}
if !s.storage.IsConnected() {
writeError(w, http.StatusServiceUnavailable, "storage not connected")
return
}
hashID = s.storage.ResolveRoot(hashID)
if err := s.storage.DeleteProperty(hashID, key); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"hashId": hashID,
"property": key,
})
}
// handleAddToSet handles POST /meta/{hash}/_add/{key}
// Adds a value to a set-type field
// Accepts JSON body with {"value": "..."} or plain text
func (s *Server) handleAddToSet(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
hashID := vars["hash"]
key := vars["key"]
if hashID == "" {
writeError(w, http.StatusBadRequest, "hash is required")
return
}
if key == "" {
writeError(w, http.StatusBadRequest, "key is required")
return
}
if validateFields(w, map[string]string{key: ""}) {
return
}
if !s.storage.IsConnected() {
writeError(w, http.StatusServiceUnavailable, "storage not connected")
return
}
hashID = s.storage.ResolveRoot(hashID)
// Read body
buf := make([]byte, 1024*1024) // 1MB max
n, err := r.Body.Read(buf)
if err != nil && err.Error() != "EOF" {
writeError(w, http.StatusBadRequest, "failed to read body")
return
}
body := buf[:n]
// Try to parse as JSON {"value": "..."} first
var propBody PropertyBody
var value string
if err := json.Unmarshal(body, &propBody); err == nil && propBody.Value != "" {
// JSON body with value field
value = propBody.Value
} else {
// Plain text body
value = string(body)
}
if value == "" {
writeError(w, http.StatusBadRequest, "value is required")
return
}
added, err := s.storage.AddToSet(hashID, key, value)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"hashId": hashID,
"property": key,
"added": added,
})
}
// CIDRequest is the request body for POST /file/cid
type CIDRequest struct {
Path string `json:"path"`