- Public signup no longer requires a caller bearer (POST /auth/signup) - create_user remains admin-only (POST /auth/users) - Optional VERAE_MCP_EDGE_URL bills global stamps via zap.verae.ai - Clearer public auth errors and instructions for Grok/clients
158 lines
3.9 KiB
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 edge.
|
|
const DefaultBaseURL = "https://zap.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]) + "…"
|
|
}
|