verae-mcp-timestamping-serv.../internal/edgebill/client.go

158 lines
3.9 KiB
Go

// Package edgebill stamps digests through the commercial edge (zap.verae.ai)
// so MCP global stamps share one meter with PrintIt / Zapier / Node-RED.
package edgebill
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// Client posts digests to the edge /v1/timestamp API.
type Client struct {
BaseURL string
HTTPClient *http.Client
Channel string
}
// DefaultBaseURL is the public commercial edge (non-Zapier).
const DefaultBaseURL = "https://services.verae.ai"
// FromEnv builds a client when VERAE_MCP_EDGE_URL is set (non-empty).
// Empty env disables edge billing (legacy direct VeraeTime only).
func FromEnv() *Client {
u := strings.TrimRight(strings.TrimSpace(os.Getenv("VERAE_MCP_EDGE_URL")), "/")
if u == "" {
return nil
}
ch := strings.TrimSpace(os.Getenv("VERAE_MCP_BILLING_CHANNEL"))
if ch == "" {
ch = "mcp"
}
return &Client{
BaseURL: u,
HTTPClient: &http.Client{Timeout: 90 * time.Second},
Channel: ch,
}
}
// Receipt is a minimal stamp result compatible with tools.ReceiptDTO mapping.
type Receipt struct {
ReceiptID string
Hash string
Scope string
CreatedAt string
Merkle string
}
// Stamp posts sha256 to edge /v1/timestamp with the caller credential.
// bearer may be an xVerae- / key- API key or a VeraeTime JWT (sent as Bearer).
func (c *Client) Stamp(ctx context.Context, sha256, bearer string) (*Receipt, error) {
if c == nil || c.BaseURL == "" {
return nil, fmt.Errorf("edgebill: not configured")
}
h := strings.ToLower(strings.TrimSpace(sha256))
h = strings.TrimPrefix(h, "sha256:")
payload, _ := json.Marshal(map[string]any{
"sha256": h,
"digest": h,
"data": h,
"hashAlg": "SHA256",
"wait": true,
"restamp": false,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/v1/timestamp", bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("x-billing-channel", c.Channel)
req.Header.Set("User-Agent", "verae-mcp-timestamping/edgebill")
tok := strings.TrimSpace(bearer)
if tok != "" {
if strings.HasPrefix(tok, "xVerae-") || strings.HasPrefix(tok, "key-") || strings.HasPrefix(tok, "zmw_") {
req.Header.Set("x-api-key", tok)
} else {
req.Header.Set("Authorization", "Bearer "+tok)
// Edge also accepts Bearer xVerae- in some paths; keep x-api-key empty.
}
}
hc := c.HTTPClient
if hc == nil {
hc = http.DefaultClient
}
res, err := hc.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
raw, _ := io.ReadAll(res.Body)
if res.StatusCode >= 300 {
return nil, fmt.Errorf("edgebill: stamp HTTP %d: %s", res.StatusCode, truncate(raw, 300))
}
var body map[string]any
_ = json.Unmarshal(raw, &body)
cid := str(body, "certificate")
if cid == "" {
cid = str(body, "receipt_id")
}
if cid == "" {
if recs, ok := body["receipts"].([]any); ok && len(recs) > 0 {
if m, ok := recs[0].(map[string]any); ok {
cid = str(m, "certificate")
if cid == "" {
cid = str(m, "receipt_id")
}
}
}
}
if cid == "" {
return nil, fmt.Errorf("edgebill: response missing certificate: %s", truncate(raw, 200))
}
return &Receipt{
ReceiptID: cid,
Hash: h,
Scope: "global",
CreatedAt: firstNonEmpty(str(body, "completedAt"), str(body, "created_at"), str(body, "timestamp")),
Merkle: firstNonEmpty(str(body, "blockHash"), str(body, "merkle")),
}, nil
}
func str(m map[string]any, k string) string {
if m == nil {
return ""
}
v, ok := m[k]
if !ok || v == nil {
return ""
}
switch t := v.(type) {
case string:
return t
default:
return fmt.Sprint(t)
}
}
func firstNonEmpty(ss ...string) string {
for _, s := range ss {
if strings.TrimSpace(s) != "" {
return s
}
}
return ""
}
func truncate(b []byte, n int) string {
if len(b) <= n {
return string(b)
}
return string(b[:n]) + "…"
}