remove in metadata cid_* to put in the cids/ set
This commit is contained in:
+12
-10
@@ -15,10 +15,10 @@ import (
|
||||
// - 404 if the CID isn't registered
|
||||
// - 503 if storage is down
|
||||
//
|
||||
// CIDs in the URL must be in prefixed-token form ("midhash256:bafk…",
|
||||
// "sha256:bafk…", "ipfs:bafy…"). Bare CIDs without an algorithm prefix
|
||||
// won't resolve — callers should pass the token they got from a previous
|
||||
// metadata document, /api/file/{cid}/info, or a meta-share federated query.
|
||||
// CIDs in the URL are bare multibase CIDv1 strings ("bafk…") — the midhash,
|
||||
// a sibling sha2-256, an IPFS CID, a btih, anything registered in the
|
||||
// reverse index. A CID is self-describing (its algorithm is the multicodec),
|
||||
// so there is no <algo>: prefix.
|
||||
//
|
||||
// This endpoint is the public read API for the CID-resolution layer and
|
||||
// is auth-bypassed alongside /api/file/{cid} so that meta-share peers and
|
||||
@@ -57,12 +57,14 @@ func (s *Server) handleGetMetaByCID(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Response envelope mirrors GetMetadataDocument JSON tags (metadata /
|
||||
// cids / duplicates), with cid added so the caller can correlate when
|
||||
// they fan out N lookups.
|
||||
// cids / canonical_cid / duplicates), with cid added so the caller can
|
||||
// correlate when they fan out N lookups. canonical_cid is derived by
|
||||
// rank from the cids key-set on read — it is not a stored field.
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"cid": cid,
|
||||
"metadata": doc.Flat,
|
||||
"cids": doc.CIDs,
|
||||
"duplicates": doc.Duplicates,
|
||||
"cid": cid,
|
||||
"metadata": doc.Flat,
|
||||
"cids": doc.CIDs,
|
||||
"canonical_cid": doc.Canonical,
|
||||
"duplicates": doc.Duplicates,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/metazla/meta-core/internal/identity"
|
||||
)
|
||||
|
||||
// identityStatusResponse is the shape of GET /api/identity. The private key
|
||||
// is intentionally absent — it is only ever returned once, by
|
||||
// POST /api/identity/generate, immediately after creation.
|
||||
type identityStatusResponse struct {
|
||||
HasIdentity bool `json:"hasIdentity"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
Curve string `json:"curve,omitempty"`
|
||||
CreatedAt int64 `json:"createdAt,omitempty"`
|
||||
}
|
||||
|
||||
type identityGenerateResponse struct {
|
||||
UID string `json:"uid"`
|
||||
Curve string `json:"curve"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
PrivateKeyHex string `json:"privateKeyHex"`
|
||||
}
|
||||
|
||||
type identityImportRequest struct {
|
||||
PrivateKeyHex string `json:"privateKeyHex"`
|
||||
}
|
||||
|
||||
type identityImportResponse struct {
|
||||
UID string `json:"uid"`
|
||||
Curve string `json:"curve"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
type identityDeleteRequest struct {
|
||||
Confirm bool `json:"confirm"`
|
||||
}
|
||||
|
||||
type identitySignRequest struct {
|
||||
BytesB64 string `json:"bytesB64"`
|
||||
}
|
||||
|
||||
type identitySignResponse struct {
|
||||
SigB64 string `json:"sigB64"`
|
||||
}
|
||||
|
||||
type identityAEADKeyResponse struct {
|
||||
AEADKeyB64 string `json:"aeadKeyB64"`
|
||||
}
|
||||
|
||||
func (s *Server) handleIdentityGet(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := identity.Load(s.config.IdentityFilePath())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if id == nil {
|
||||
writeJSON(w, http.StatusOK, identityStatusResponse{HasIdentity: false})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, identityStatusResponse{
|
||||
HasIdentity: true,
|
||||
UID: id.UID,
|
||||
Curve: id.Curve,
|
||||
CreatedAt: id.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleIdentityGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
if identity.Exists(s.config.IdentityFilePath()) {
|
||||
writeErrorSlug(w, http.StatusConflict, "identity_exists",
|
||||
"identity already exists; delete it first to generate a new one", false)
|
||||
return
|
||||
}
|
||||
id, err := identity.Generate()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if err := identity.Save(s.config.IdentityFilePath(), id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, identityGenerateResponse{
|
||||
UID: id.UID,
|
||||
Curve: id.Curve,
|
||||
CreatedAt: id.CreatedAt,
|
||||
PrivateKeyHex: id.PrivateKeyHex,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleIdentityImport(w http.ResponseWriter, r *http.Request) {
|
||||
if identity.Exists(s.config.IdentityFilePath()) {
|
||||
writeErrorSlug(w, http.StatusConflict, "identity_exists",
|
||||
"identity already exists; delete it first to import a new one", false)
|
||||
return
|
||||
}
|
||||
var body identityImportRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(body.PrivateKeyHex) == "" {
|
||||
writeError(w, http.StatusBadRequest, "privateKeyHex is required")
|
||||
return
|
||||
}
|
||||
id, err := identity.Import(strings.TrimSpace(body.PrivateKeyHex))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := identity.Save(s.config.IdentityFilePath(), id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, identityImportResponse{
|
||||
UID: id.UID,
|
||||
Curve: id.Curve,
|
||||
CreatedAt: id.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleIdentityDelete(w http.ResponseWriter, r *http.Request) {
|
||||
var body identityDeleteRequest
|
||||
// Tolerate empty body — confirm flag in a query param is also acceptable
|
||||
// for shells where sending a JSON body with DELETE is awkward.
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
if !body.Confirm && r.URL.Query().Get("confirm") != "true" {
|
||||
writeErrorSlug(w, http.StatusBadRequest, "confirm_required",
|
||||
"identity deletion requires {\"confirm\": true} or ?confirm=true", false)
|
||||
return
|
||||
}
|
||||
if err := identity.Delete(s.config.IdentityFilePath()); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handleIdentitySign(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := identity.Load(s.config.IdentityFilePath())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if id == nil {
|
||||
writeErrorSlug(w, http.StatusNotFound, "no_identity",
|
||||
"no identity is configured", false)
|
||||
return
|
||||
}
|
||||
var body identitySignRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(body.BytesB64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bytesB64 is not valid base64")
|
||||
return
|
||||
}
|
||||
sig, err := identity.Sign(id, raw)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, identitySignResponse{
|
||||
SigB64: base64.StdEncoding.EncodeToString(sig),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleIdentityAEADKey(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := identity.Load(s.config.IdentityFilePath())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if id == nil {
|
||||
writeErrorSlug(w, http.StatusNotFound, "no_identity",
|
||||
"no identity is configured", false)
|
||||
return
|
||||
}
|
||||
key, err := identity.DeriveAEADKey(id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, identityAEADKeyResponse{
|
||||
AEADKeyB64: base64.StdEncoding.EncodeToString(key),
|
||||
})
|
||||
}
|
||||
@@ -155,6 +155,15 @@ func (s *Server) setupRoutes() {
|
||||
s.router.HandleFunc("/api/metadata/batch", s.handleBatchUpdate).Methods("POST")
|
||||
s.router.HandleFunc("/api/metadata/clear", s.handleClearMetadata).Methods("POST")
|
||||
|
||||
// User Data Layer signing identity (see internal/identity). Auth-gated
|
||||
// at the perimeter — do NOT add to nginx-hash-lock unauth bypass.
|
||||
s.router.HandleFunc("/api/identity", s.handleIdentityGet).Methods("GET")
|
||||
s.router.HandleFunc("/api/identity/generate", s.handleIdentityGenerate).Methods("POST")
|
||||
s.router.HandleFunc("/api/identity/import", s.handleIdentityImport).Methods("POST")
|
||||
s.router.HandleFunc("/api/identity", s.handleIdentityDelete).Methods("DELETE")
|
||||
s.router.HandleFunc("/api/identity/sign", s.handleIdentitySign).Methods("POST")
|
||||
s.router.HandleFunc("/api/identity/aead-key", s.handleIdentityAEADKey).Methods("GET")
|
||||
|
||||
// Snapshot (export / import / wipe)
|
||||
s.router.HandleFunc("/api/snapshot/export", s.handleSnapshotExport).Methods("GET")
|
||||
s.router.HandleFunc("/api/snapshot/import", s.handleSnapshotImport).Methods("POST")
|
||||
|
||||
Reference in New Issue
Block a user