verae-mcp-timestamping-serv.../internal/tools/public.go
George Lambert 3ec1f0516b
Some checks failed
test-and-deploy / test (push) Failing after 12m55s
test-and-deploy / deploy-138 (push) Has been cancelled
feat(mcp): optional receipt_format on stamp/verify (text/PDF/JSON/MD/CSV/TSV)
Add receipt_format to stamp and verify. Omitted → text; receipt|PDF-Receipt → PDF;
JSON-Receipt → JSON; MD|MD-Receipt; CSV|CSV-Receipt; TSV|TSV-Receipt.

Stamp always returns receipts[] and adds artifacts[] in the chosen format.
Stdio writes ~/.verae/receipts (0600); public MCP returns content_text/content_base64 only.
Verify accepts inline receipt or receipt_path (stdio), parses id, and re-emits an artifact.

Vendors pdfcert (+ slim statuslang, gofpdf) for CERT-01 PDF. Also lands welcome tool
and email-username helpers already wired into register/public paths.
2026-10-20 16:02:20 -04:00

119 lines
4.4 KiB
Go

package tools
// DEC-654 (D): on the public HTTP server (mcp.verae.ai) account and auth are required for use. Every tool except
// welcome, signup, and login needs the caller's own credential — a VeraeTime JWT (access_token from signup/login)
// or a Verae API key — checked at api.veraetime.net — and never falls back to this host's stored service login.
// Tools that need a caller bearer: stamp, stamp_batch, verify, hash_report, session, create_user.
// Public signup and login are open (no bearer): they create/authenticate the account and return access_token so the
// client can set Authorization / bearer_token_file. They never write the host auth.properties on the public server.
// welcome is open so new users can read onboarding before signing up.
//
// AI clients (Grok, Claude, Cursor) must send Authorization: Bearer on every tools/call after signup/login. Grok Build:
// bearer_token_file = "~/.verae/mcp-bearer" # raw JWT only, not auth.properties
// or point a local stdio MCP at this binary (Public=false) so the laptop host token is used.
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 when the public server is used without a caller account/credential.
var ErrWriteNeedsKey = errors.New(
"account and auth are required for use. " +
"Call tool welcome for onboarding, then signup (new account) or login (existing) to receive a VeraeTime JWT as access_token. " +
"Write that token to your client's bearer file (Grok: ~/.verae/mcp-bearer via bearer_token_file) " +
"or use a Verae API key from https://zap.verae.ai/portal/, " +
"then send Authorization: Bearer on each tools/call. " +
"Or run this binary with -transport stdio so the laptop host login is used",
)
// tokenHowToUse is guidance returned with access_token from public signup/login.
const tokenHowToUse = "Store access_token as the only contents of your MCP bearer file " +
"(Grok: ~/.verae/mcp-bearer with mcp_servers.<name>.bearer_token_file), " +
"or send Authorization: Bearer <access_token> on each tools/call. " +
"Do not paste the token into chat. Commercial global stamps also accept an xVerae- portal API key."
// 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 requires the caller's own credential on the public server (account/auth required for use).
// On stdio/local (Public=false) it may fall back to the host login via resolveBearer.
func (d *Deps) readBearer(ctx context.Context, req *mcp.CallToolRequest) (string, error) {
if d.Public {
return d.writeBearer(ctx, req)
}
return d.resolveBearer(ctx, req)
}
// requireAccount gates any public tool that is not welcome/signup/login.
func (d *Deps) requireAccount(ctx context.Context, req *mcp.CallToolRequest) error {
if !d.Public {
return nil
}
_, err := d.writeBearer(ctx, req)
return err
}
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
}