package tools // DEC-654 (D): on the public HTTP server (mcp.verae.ai) every write needs the caller's own credential - a VeraeTime // JWT (access_token from login) or a Verae API key - checked at api.veraetime.net - and never falls back to this // host's stored service login. Writes that need a caller bearer: stamp (any scope), create_user, session, // stamp_batch. Public signup is open (POST /auth/signup, no bearer) so new customers can register, then stamp // with their own JWT. login is refused on the public server (it would replace the host credential). // Read-only verify and hash_report stay anonymous; they may use the host login and are never rate limited (DEC-660). // // AI clients (Grok, Claude, Cursor) must send Authorization: Bearer on every tools/call. 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 for an anonymous write on the public server. var ErrWriteNeedsKey = errors.New( "this tool needs your own Verae credential on Authorization: Bearer. " + "Use a VeraeTime JWT (access_token from `verae login` / ~/.verae/auth.properties) " + "or a Verae API key from https://zap.verae.ai/portal/. " + "Grok Build: set mcp_servers.verae-timestamping.bearer_token_file to a file that holds only the raw token " + "(e.g. ~/.verae/mcp-bearer), then reconnect the MCP server; " + "or run this binary with -transport stdio so the laptop host login is used", ) // ErrLoginDisabled is returned by the login tool on the public server. var ErrLoginDisabled = errors.New( "login is not available on the public HTTP server (it would replace the host credential). " + "Sign in with `verae login` or at https://zap.verae.ai/portal/, put the JWT or API key in " + "Authorization: Bearer on each MCP request (Grok: bearer_token_file), " + "or use a local stdio MCP transport", ) // 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 }