verae-mcp-timestamping-serv.../internal/tools/public.go

93 lines
3.1 KiB
Go

package tools
// DEC-654 (D): on the public HTTP server (mcp.verae.ai) every write needs the caller's own credential - a Verae API
// key or a VeraeTime login/OAuth bearer, checked at api.veraetime.net - and never falls back to this host's stored
// service login. Writes: stamp (any scope; global stamps are billed to the key's customer), signup, create_user,
// session, stamp_batch; login (it would replace the host credential) is refused. Read-only verify and hash_report
// stay anonymous for the public pages (receipt links, explorer); only those use the host login, and they are never
// rate limited (DEC-660, George 2026-10-10: "never limit").
import (
"context"
"crypto/sha256"
"errors"
"sync"
"time"
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/veraetime"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// ErrWriteNeedsKey is returned for an anonymous write on the public server.
var ErrWriteNeedsKey = errors.New("this tool needs your own Verae credential: send Authorization: Bearer <Verae API key> " +
"(create one at https://zap.verae.ai/portal/ or run `verae login`)")
// ErrLoginDisabled is returned by the login tool on the public server.
var ErrLoginDisabled = errors.New("login is not available on the public server; sign in at https://zap.verae.ai/portal/ " +
"(or `verae login`) and send the Verae API key as a bearer token")
// CredentialChecker validates a caller bearer (implemented by veraetime.Client.CheckCredential).
type CredentialChecker interface {
CheckCredential(ctx context.Context, bearer string) error
}
// writeBearer returns the caller's own validated bearer; on the public server there is no host fallback.
func (d *Deps) writeBearer(ctx context.Context, req *mcp.CallToolRequest) (string, error) {
if !d.Public {
return d.resolveBearer(ctx, req)
}
tok := BearerFromRequest(req)
if tok == "" {
return "", ErrWriteNeedsKey
}
if err := d.checkCred(ctx, tok); err != nil {
return "", err
}
return tok, nil
}
// readBearer is the caller's bearer, or (anonymous, never limited - DEC-660) the host login for read-only lookups.
func (d *Deps) readBearer(ctx context.Context, req *mcp.CallToolRequest) (string, error) {
return d.resolveBearer(ctx, req)
}
type credEntry struct {
ok bool
exp time.Time
}
var (
credMu sync.Mutex
credCache = map[[32]byte]credEntry{}
)
func (d *Deps) checkCred(ctx context.Context, tok string) error {
if d.Checker == nil {
return ErrWriteNeedsKey // fail closed
}
h := sha256.Sum256([]byte(tok))
credMu.Lock()
e, hit := credCache[h]
credMu.Unlock()
if hit && time.Now().Before(e.exp) {
if e.ok {
return nil
}
return errors.New("the Verae credential was not accepted")
}
err := d.Checker.CheckCredential(ctx, tok)
var bad *veraetime.CredentialRejected
if err != nil && !errors.As(err, &bad) {
return err // upstream unavailable: not cached, fail closed
}
credMu.Lock()
if len(credCache) > 10000 {
credCache = map[[32]byte]credEntry{}
}
credCache[h] = credEntry{ok: err == nil, exp: time.Now().Add(60 * time.Second)}
credMu.Unlock()
if err != nil {
return errors.New("the Verae credential was not accepted")
}
return nil
}