From 13d1c5851b2ca2450c199fbcd68ed28883af3afc Mon Sep 17 00:00:00 2001 From: George Lambert Date: Sat, 17 Oct 2026 00:26:50 -0400 Subject: [PATCH] fix(mcp): require xVerae- commercial key when EDGE_URL set; batch via edge --- internal/edgebill/client.go | 4 +-- internal/tools/handlers.go | 49 ++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/internal/edgebill/client.go b/internal/edgebill/client.go index c9d9e71..a944611 100644 --- a/internal/edgebill/client.go +++ b/internal/edgebill/client.go @@ -21,8 +21,8 @@ type Client struct { Channel string } -// DefaultBaseURL is the public edge. -const DefaultBaseURL = "https://zap.verae.ai" +// 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). diff --git a/internal/tools/handlers.go b/internal/tools/handlers.go index 16ef37d..0a35976 100644 --- a/internal/tools/handlers.go +++ b/internal/tools/handlers.go @@ -30,6 +30,14 @@ func (d *Deps) resolveBearer(ctx context.Context, req *mcp.CallToolRequest) (str return authctx.Resolve(ctx, BearerFromRequest(req), d.RequireBearer, d.Keeper) } +// isCommercialEdgeKey reports tokens that edge apiKeyAuth can map to a prepaid customer. +func isCommercialEdgeKey(tok string) bool { + t := strings.TrimSpace(tok) + return strings.HasPrefix(t, "xVerae-") || + strings.HasPrefix(t, "key-") || + strings.HasPrefix(t, "zmw_") +} + func (d *Deps) stamp(ctx context.Context, req *mcp.CallToolRequest, in StampArgs) (*mcp.CallToolResult, ReceiptsOut, error) { if err := assertDigestArgs(req); err != nil { return nil, ReceiptsOut{}, err @@ -49,8 +57,15 @@ func (d *Deps) stamp(ctx context.Context, req *mcp.CallToolRequest, in StampArgs if err != nil { return nil, ReceiptsOut{}, err } - // Single commercial meter: when Edge is configured, bill via zap.verae.ai. + // Single commercial meter: when Edge is configured, bill via services.verae.ai. + // Caller must present a commercial key (xVerae- / key- / zmw_), not a bare chain JWT, + // so settleStampBilling attributes the customer prepaid ledger. if d.Edge != nil { + if !isCommercialEdgeKey(tok) { + return nil, ReceiptsOut{}, fmt.Errorf( + "commercial stamp requires portal API key (xVerae-… or key-…); get one at https://customer.verae.ai/portal/ — bare VeraeTime JWT is not billed on MCP when VERAE_MCP_EDGE_URL is set", + ) + } if len(in.Metadata) > 0 || in.Restamp { return nil, ReceiptsOut{}, fmt.Errorf("metadata/restamp on edge-billed stamps is not supported yet; omit metadata or unset VERAE_MCP_EDGE_URL") } @@ -121,13 +136,39 @@ func (d *Deps) stampBatch(ctx context.Context, req *mcp.CallToolRequest, in Stam } switch scope { case "global": - if d.Global == nil { - return nil, StampBatchOut{}, fmt.Errorf("global VeraeTime client not configured") - } tok, err := d.writeBearer(ctx, req) if err != nil { return nil, StampBatchOut{}, err } + // Prefer edge meter (per-digest) when configured — same prepaid path as single stamp. + if d.Edge != nil { + if !isCommercialEdgeKey(tok) { + return nil, StampBatchOut{}, fmt.Errorf( + "commercial stamp requires portal API key (xVerae-… or key-…); get one at https://customer.verae.ai/portal/", + ) + } + if len(in.Metadata) > 0 || in.Restamp { + return nil, StampBatchOut{}, fmt.Errorf("metadata/restamp on edge-billed batch not supported yet") + } + out := StampBatchOut{Count: len(in.SHA256s)} + for i, h := range in.SHA256s { + it := BatchItemDTO{Index: i, Hash: h, Status: "submitted"} + er, err := d.Edge.Stamp(ctx, h, tok) + if err != nil { + it.Status, it.Error = "failed", err.Error() + out.Failed++ + } else { + it.ReceiptID = er.ReceiptID + it.Status = "completed" + out.Created++ + } + out.Items = append(out.Items, it) + } + return nil, out, nil + } + if d.Global == nil { + return nil, StampBatchOut{}, fmt.Errorf("global VeraeTime client not configured") + } var metas []veraetime.StampMeta if len(in.Metadata) > 0 || in.Restamp { if len(in.Metadata) > 0 && len(in.Metadata) != len(in.SHA256s) {