tether

package module
v1.0.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 4, 2026 License: MIT Imports: 17 Imported by: 0

README

tether-go

Go Reference Go Report Card MIT License

A Go client library for Tether - cryptographic identity verification for AI agents. Tether lets AI agents prove their identity using RSA digital signatures, and manage agents programmatically with bearer auth (JWT or API keys).

Installation

go get github.com/tether-name/tether-name-go

Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    
    "github.com/tether-name/tether-name-go"
)

func main() {
    // Initialize client with agent ID and private key
    client, err := tether.NewClient(tether.Options{
        AgentID:   "your-agent-id",
        PrivateKeyPath: "/path/to/private-key.pem",
    })
    if err != nil {
        log.Fatal(err)
    }
    
    // Verify agent identity
    result, err := client.Verify(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Verified: %t\n", result.Verified)
    fmt.Printf("Agent: %s\n", result.AgentName)
    fmt.Printf("Verify URL: %s\n", result.VerifyURL)
}

Agent Management

Use a bearer token (Authorization: Bearer ..., JWT or API key) to create, update, list, and delete agents programmatically:

client, err := tether.NewClient(tether.Options{
    ApiKey: "sk-tether-name-...",
})
if err != nil {
    log.Fatal(err)
}

ctx := context.Background()

// Create a new agent
agent, err := client.CreateAgent(ctx, "my-bot", "My AI assistant")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Created agent: %s (ID: %s)\n", agent.AgentName, agent.ID)

// Create an agent with a verified domain
domains, err := client.ListDomains(ctx)
if err != nil {
    log.Fatal(err)
}
if len(domains) > 0 && domains[0].Verified {
    agent, err := client.CreateAgent(ctx, "my-bot", "My AI assistant", domains[0].ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created agent with domain: %s\n", agent.AgentName)
}

// List all agents
agents, err := client.ListAgents(ctx)
if err != nil {
    log.Fatal(err)
}
for _, a := range agents {
    fmt.Printf("  %s (%s)\n", a.AgentName, a.ID)
}

// Update which identity is shown when this agent is verified
// Pass a verified domain ID to show that domain:
_, err = client.UpdateAgentDomain(ctx, agent.ID, "verified-domain-id")
if err != nil {
    log.Fatal(err)
}
// Pass empty string to show account email:
_, err = client.UpdateAgentDomain(ctx, agent.ID, "")
if err != nil {
    log.Fatal(err)
}

// Delete an agent
deleted, err := client.DeleteAgent(ctx, agent.ID)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Deleted: %t\n", deleted)

// List key lifecycle entries
keys, err := client.ListAgentKeys(ctx, agent.ID)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Keys: %d\n", len(keys))

// Rotate key (step-up required: stepUpCode OR challenge+proof)
rotated, err := client.RotateAgentKey(ctx, agent.ID, tether.RotateAgentKeyRequest{
    PublicKey:        "BASE64_SPKI_PUBLIC_KEY",
    GracePeriodHours: 24,
    Reason:           "routine_rotation",
    StepUpCode:       "123456",
})
if err != nil {
    log.Fatal(err)
}

// Revoke a specific key
_, err = client.RevokeAgentKey(ctx, agent.ID, rotated.NewKeyID, tether.RevokeAgentKeyRequest{
    Reason:     "compromised",
    StepUpCode: "654321",
})
if err != nil {
    log.Fatal(err)
}

When using a bearer token in ApiKey (JWT or API key), AgentID and private key options become optional — they're only needed for verify/sign operations.

Step-by-Step Verification

For more control over the verification process:

package main

import (
    "context"
    "fmt"
    "log"
    
    "github.com/tether-name/tether-name-go"
)

func main() {
    client, err := tether.NewClient(tether.Options{
        AgentID:   "your-agent-id",
        PrivateKeyPath: "/path/to/private-key.pem",
    })
    if err != nil {
        log.Fatal(err)
    }
    
    ctx := context.Background()
    
    // Step 1: Request a challenge
    challenge, err := client.RequestChallenge(ctx)
    if err != nil {
        log.Fatal("Failed to request challenge:", err)
    }
    fmt.Printf("Challenge: %s\n", challenge)
    
    // Step 2: Sign the challenge
    proof, err := client.Sign(challenge)
    if err != nil {
        log.Fatal("Failed to sign challenge:", err)
    }
    fmt.Printf("Proof: %s\n", proof)
    
    // Step 3: Submit proof for verification
    result, err := client.SubmitProof(ctx, challenge, proof)
    if err != nil {
        log.Fatal("Failed to verify proof:", err)
    }
    
    fmt.Printf("Verification Result:\n")
    fmt.Printf("  Verified: %t\n", result.Verified)
    fmt.Printf("  Agent: %s\n", result.AgentName)
    fmt.Printf("  Email: %s\n", result.Email)
    fmt.Printf("  Verify URL: %s\n", result.VerifyURL)
    if result.RegisteredSince != nil {
        fmt.Printf("  Registered: %s\n", result.RegisteredSince.Format("2006-01-02"))
    }
}

Private Key Formats

The library supports multiple ways to provide your RSA private key:

Full Options
client, err := tether.NewClient(tether.Options{
    AgentID:   "your-agent-id",
    PrivateKeyPath: "/path/to/key.pem",              // PEM or DER format
    ApiKey:         "sk-tether-name-...",                 // Optional, for agent management
})
PEM Bytes
pemData := []byte(`-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----`)

client, err := tether.NewClient(tether.Options{
    AgentID:  "your-agent-id",
    PrivateKeyPEM: pemData,
})
DER Bytes
client, err := tether.NewClient(tether.Options{
    AgentID:  "your-agent-id",
    PrivateKeyDER: derBytes, // Raw DER-encoded key bytes
})

Environment Variables

You can use environment variables as fallbacks:

export TETHER_AGENT_ID="your-agent-id"
export TETHER_PRIVATE_KEY_PATH="/path/to/private-key.pem"
export TETHER_API_KEY="sk-tether-name-..."
// Will use environment variables if options are empty
client, err := tether.NewClient(tether.Options{})

API Reference

Client Creation
NewClient(opts Options) (*TetherClient, error)

Creates a new Tether client with the specified options.

Options:

  • AgentID (string): Your unique agent ID (required for verify/sign, optional with bearer token)
  • PrivateKeyPath (string): Path to RSA private key file (PEM or DER format)
  • PrivateKeyPEM ([]byte): RSA private key in PEM format
  • PrivateKeyDER ([]byte): RSA private key in DER format
  • ApiKey (string): Management bearer token (JWT or API key; falls back to TETHER_API_KEY env var)
Client Methods
Verify(ctx context.Context) (*VerificationResult, error)

Performs complete verification flow: requests challenge, signs it, and submits proof.

RequestChallenge(ctx context.Context) (string, error)

Requests a new challenge from the Tether API.

Sign(challenge string) (string, error)

Signs a challenge using the client's private key. Returns URL-safe base64 signature (no padding).

SubmitProof(ctx context.Context, challenge, proof string) (*VerificationResult, error)

Submits signed challenge proof for verification.

CreateAgent(ctx context.Context, agentName, description string, domainID ...string) (*Agent, error)

Creates a new agent. Optionally pass a verified domain ID. Requires bearer auth (JWT or API key).

ListAgents(ctx context.Context) ([]Agent, error)

Lists all agents for the authenticated user. Requires bearer auth (JWT or API key).

DeleteAgent(ctx context.Context, agentID string) (bool, error)

Deletes an agent by ID. Requires bearer auth (JWT or API key).

UpdateAgentDomain(ctx context.Context, agentID string, domainID string) (*UpdateAgentResponse, error)

Updates which identity is shown when an agent is verified. Pass a verified domainID to show that domain, or pass "" (empty string) to show account email. Requires bearer auth (JWT or API key).

ListDomains(ctx context.Context) ([]Domain, error)

Lists all registered domains for the authenticated user. Requires bearer auth (JWT or API key).

ListAgentKeys(ctx context.Context, agentID string) ([]AgentKey, error)

Lists key lifecycle entries (active, grace, revoked) for an agent. Requires bearer auth (JWT or API key).

RotateAgentKey(ctx context.Context, agentID string, req RotateAgentKeyRequest) (*RotateAgentKeyResponse, error)

Rotates an agent key. Requires bearer auth (JWT or API key) and step-up verification via either StepUpCode or Challenge + Proof.

RevokeAgentKey(ctx context.Context, agentID, keyID string, req RevokeAgentKeyRequest) (*RevokeAgentKeyResponse, error)

Revokes an agent key. Requires bearer auth (JWT or API key) and step-up verification via either StepUpCode or Challenge + Proof.

Types
VerificationResult

Result returned by Verify, RequestChallenge, and SubmitProof.

Field Type Description
Verified bool Whether verification succeeded
AgentName string Registered agent name
VerifyURL string Public verification URL
Email string Registered owner email
Domain string Verified domain (if assigned to this agent)
RegisteredSince *EpochTime Registration timestamp
Error string Error message if verification failed
Challenge string The verified challenge code
type VerificationResult struct {
    Verified        bool       `json:"verified"`
    AgentName       string     `json:"agentName,omitempty"`
    VerifyURL       string     `json:"verifyUrl,omitempty"`
    Email           string     `json:"email,omitempty"`
    Domain          string     `json:"domain,omitempty"`
    RegisteredSince *EpochTime `json:"registeredSince,omitempty"`
    Error           string     `json:"error,omitempty"`
    Challenge       string     `json:"challenge,omitempty"`
}
Agent

Agent returned by management operations.

Field Type Description
ID string Unique agent ID
AgentName string Agent display name
Description string Agent description
DomainID string Assigned domain ID (if any)
Domain string Assigned domain name (if any)
CreatedAt int64 Creation time (epoch ms)
RegistrationToken string Token for key registration (returned on create)
LastVerifiedAt int64 Last verification time (epoch ms)
type Agent struct {
    ID                string `json:"id"`
    AgentName         string `json:"agentName"`
    Description       string `json:"description"`
    DomainID          string `json:"domainId,omitempty"`
    Domain            string `json:"domain,omitempty"`
    CreatedAt         int64  `json:"createdAt"`
    RegistrationToken string `json:"registrationToken,omitempty"`
    LastVerifiedAt    int64  `json:"lastVerifiedAt,omitempty"`
}
Domain

Domain returned by ListDomains.

Field Type Description
ID string Unique domain ID
Domain string Domain name
Verified bool Whether the domain is verified
VerifiedAt int64 Verification time (epoch ms)
LastCheckedAt int64 Last re-verification check (epoch ms)
CreatedAt int64 Creation time (epoch ms)
type Domain struct {
    ID            string `json:"id"`
    Domain        string `json:"domain"`
    Verified      bool   `json:"verified"`
    VerifiedAt    int64  `json:"verifiedAt,omitempty"`
    LastCheckedAt int64  `json:"lastCheckedAt,omitempty"`
    CreatedAt     int64  `json:"createdAt,omitempty"`
}
AgentKey

Key lifecycle entry returned by ListAgentKeys.

type AgentKey struct {
    ID            string `json:"id"`
    Status        string `json:"status"` // active | grace | revoked
    CreatedAt     int64  `json:"createdAt"`
    ActivatedAt   int64  `json:"activatedAt"`
    GraceUntil    int64  `json:"graceUntil"`
    RevokedAt     int64  `json:"revokedAt"`
    RevokedReason string `json:"revokedReason,omitempty"`
}
Error Types

The library provides custom error types for different failure scenarios:

  • VerificationError: Verification failed (invalid signature, etc.)
  • APIError: Network or HTTP errors
  • KeyLoadError: Private key loading errors

How Tether Works

  1. Registration: Register your AI agent at tether.name to get an agent ID, then generate an RSA key pair locally and register the public key
  2. Challenge: Request a unique challenge code from the Tether API
  3. Signature: Sign the challenge with your private key using SHA256withRSA
  4. Verification: Submit the challenge and signature for verification
  5. Proof: Receive verification result with public verify URL

Requirements

  • Go 1.22 or later
  • RSA-2048 private key (PEM or DER format)
  • No external dependencies (uses only Go standard library)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Publishing

Go modules are published via git tags — no registry upload needed.

Version checklist

Update the version in:

  1. client.goUserAgent constant
Steps
  1. Update the UserAgent version string above
  2. Commit and push to main
  3. Tag the release: git tag v1.0.0 && git push --tags
  4. The module is immediately available via go get github.com/tether-name/tether-name-go@v1.0.0

Documentation

Index

Constants

View Source
const (
	// DefaultBaseURL is the default Tether API base URL
	DefaultBaseURL = "https://api.tether.name"

	// UserAgent for HTTP requests
	UserAgent = "tether-go/1.0.8"
)

Variables

View Source
var ErrAPI = errors.New("API error")

ErrAPI indicates an API communication error

View Source
var ErrKeyLoad = errors.New("key load error")

ErrKeyLoad indicates a private key loading error

View Source
var ErrVerification = errors.New("verification failed")

ErrVerification indicates a verification failure

Functions

This section is empty.

Types

type APIError

type APIError struct {
	StatusCode int
	Message    string
	Err        error
}

APIError represents an API communication error

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Unwrap

func (e *APIError) Unwrap() error

type Agent added in v1.0.2

type Agent struct {
	ID                string `json:"id"`
	AgentName         string `json:"agentName"`
	Description       string `json:"description"`
	DomainID          string `json:"domainId,omitempty"`
	Domain            string `json:"domain,omitempty"`
	CreatedAt         int64  `json:"createdAt"`
	RegistrationToken string `json:"registrationToken,omitempty"`
	LastVerifiedAt    int64  `json:"lastVerifiedAt,omitempty"`
}

Agent represents a registered agent

type AgentKey added in v1.0.6

type AgentKey struct {
	ID            string `json:"id"`
	Status        string `json:"status"`
	CreatedAt     int64  `json:"createdAt"`
	ActivatedAt   int64  `json:"activatedAt"`
	GraceUntil    int64  `json:"graceUntil"`
	RevokedAt     int64  `json:"revokedAt"`
	RevokedReason string `json:"revokedReason,omitempty"`
}

AgentKey represents a key lifecycle entry for an agent.

type Domain added in v1.0.5

type Domain struct {
	ID            string `json:"id"`
	Domain        string `json:"domain"`
	Verified      bool   `json:"verified"`
	VerifiedAt    int64  `json:"verifiedAt,omitempty"`
	LastCheckedAt int64  `json:"lastCheckedAt,omitempty"`
	CreatedAt     int64  `json:"createdAt,omitempty"`
}

Domain represents a registered domain under the authenticated account.

type EpochTime added in v1.0.1

type EpochTime struct {
	time.Time
}

EpochTime wraps time.Time to handle both epoch millisecond integers and ISO 8601 strings when unmarshaling JSON.

func (EpochTime) MarshalJSON added in v1.0.1

func (et EpochTime) MarshalJSON() ([]byte, error)

MarshalJSON outputs as epoch milliseconds for round-trip consistency.

func (*EpochTime) UnmarshalJSON added in v1.0.1

func (et *EpochTime) UnmarshalJSON(data []byte) error

UnmarshalJSON handles epoch ms (number) or ISO 8601 (string).

type KeyLoadError

type KeyLoadError struct {
	Message string
	Err     error
}

KeyLoadError represents a private key loading error

func (*KeyLoadError) Error

func (e *KeyLoadError) Error() string

func (*KeyLoadError) Unwrap

func (e *KeyLoadError) Unwrap() error

type Options

type Options struct {
	// AgentID is the unique identifier for this agent (required for verify/sign operations)
	AgentID string

	// PrivateKeyPath is the file path to the RSA private key (PEM or DER format)
	PrivateKeyPath string

	// PrivateKeyPEM contains the RSA private key in PEM format as bytes
	PrivateKeyPEM []byte

	// PrivateKeyDER contains the RSA private key in DER format as bytes
	PrivateKeyDER []byte

	// ApiKey for management operations (alternative to agent auth)
	ApiKey string
}

Options configures the TetherClient

type RevokeAgentKeyRequest added in v1.0.6

type RevokeAgentKeyRequest struct {
	Reason     string `json:"reason,omitempty"`
	StepUpCode string `json:"stepUpCode,omitempty"`
	Challenge  string `json:"challenge,omitempty"`
	Proof      string `json:"proof,omitempty"`
}

RevokeAgentKeyRequest defines payload for revoking an agent key.

type RevokeAgentKeyResponse added in v1.0.6

type RevokeAgentKeyResponse struct {
	AgentID       string `json:"agentId"`
	KeyID         string `json:"keyId"`
	Revoked       bool   `json:"revoked"`
	PromotedKeyID string `json:"promotedKeyId,omitempty"`
	Message       string `json:"message"`
}

RevokeAgentKeyResponse is returned from key revoke endpoint.

type RotateAgentKeyRequest added in v1.0.6

type RotateAgentKeyRequest struct {
	PublicKey        string `json:"publicKey"`
	GracePeriodHours int    `json:"gracePeriodHours,omitempty"`
	Reason           string `json:"reason,omitempty"`
	StepUpCode       string `json:"stepUpCode,omitempty"`
	Challenge        string `json:"challenge,omitempty"`
	Proof            string `json:"proof,omitempty"`
}

RotateAgentKeyRequest defines payload for rotating an agent key.

type RotateAgentKeyResponse added in v1.0.6

type RotateAgentKeyResponse struct {
	AgentID       string `json:"agentId"`
	PreviousKeyID string `json:"previousKeyId,omitempty"`
	NewKeyID      string `json:"newKeyId"`
	GraceUntil    int64  `json:"graceUntil"`
	Message       string `json:"message"`
}

RotateAgentKeyResponse is returned from key rotation endpoint.

type TetherClient

type TetherClient struct {
	// contains filtered or unexported fields
}

TetherClient represents a client for the Tether API

func NewClient

func NewClient(opts Options) (*TetherClient, error)

NewClient creates a new TetherClient with the given options. When ApiKey is provided, agentID and privateKey become optional (only required for verify/sign operations).

func (*TetherClient) CreateAgent added in v1.0.2

func (c *TetherClient) CreateAgent(ctx context.Context, agentName string, description string, domainID ...string) (*Agent, error)

CreateAgent creates a new agent. Requires an API key to be configured. domainID is optional. When provided, it assigns this agent to that verified domain.

func (*TetherClient) DeleteAgent added in v1.0.2

func (c *TetherClient) DeleteAgent(ctx context.Context, agentID string) (bool, error)

DeleteAgent deletes an agent by ID. Requires an API key to be configured.

func (*TetherClient) ListAgentKeys added in v1.0.6

func (c *TetherClient) ListAgentKeys(ctx context.Context, agentID string) ([]AgentKey, error)

ListAgentKeys lists key lifecycle entries for an agent.

func (*TetherClient) ListAgents added in v1.0.2

func (c *TetherClient) ListAgents(ctx context.Context) ([]Agent, error)

ListAgents lists all agents for the authenticated user. Requires an API key to be configured.

func (*TetherClient) ListDomains added in v1.0.5

func (c *TetherClient) ListDomains(ctx context.Context) ([]Domain, error)

ListDomains lists all registered domains for the authenticated user. Requires an API key to be configured.

func (*TetherClient) RequestChallenge

func (c *TetherClient) RequestChallenge(ctx context.Context) (string, error)

RequestChallenge requests a new challenge from the Tether API

func (*TetherClient) RevokeAgentKey added in v1.0.6

func (c *TetherClient) RevokeAgentKey(ctx context.Context, agentID, keyID string, reqBody RevokeAgentKeyRequest) (*RevokeAgentKeyResponse, error)

RevokeAgentKey revokes an agent key with optional step-up auth.

func (*TetherClient) RotateAgentKey added in v1.0.6

func (c *TetherClient) RotateAgentKey(ctx context.Context, agentID string, reqBody RotateAgentKeyRequest) (*RotateAgentKeyResponse, error)

RotateAgentKey rotates an agent key with optional step-up auth.

func (*TetherClient) Sign

func (c *TetherClient) Sign(challenge string) (string, error)

Sign signs a challenge using the client's private key. Requires a private key to be configured.

func (*TetherClient) SubmitProof

func (c *TetherClient) SubmitProof(ctx context.Context, challenge, proof string) (*VerificationResult, error)

SubmitProof submits a signed challenge proof for verification. Requires agentID to be configured.

func (*TetherClient) UpdateAgentDomain added in v1.0.8

func (c *TetherClient) UpdateAgentDomain(ctx context.Context, agentID string, domainID string) (*UpdateAgentResponse, error)

UpdateAgentDomain updates which identity is shown when an agent is verified. Pass a verified domainID to show that domain, or pass an empty string to show account email.

func (*TetherClient) Verify

Verify performs a complete verification flow (request challenge + sign + verify)

type UpdateAgentResponse added in v1.0.8

type UpdateAgentResponse struct {
	ID       string `json:"id"`
	DomainID string `json:"domainId,omitempty"`
	Domain   string `json:"domain,omitempty"`
	Message  string `json:"message,omitempty"`
}

UpdateAgentResponse is the response payload for PATCH /agents/{id}

type VerificationError

type VerificationError struct {
	Message string
	Err     error
}

VerificationError represents a verification failure with details

func (*VerificationError) Error

func (e *VerificationError) Error() string

func (*VerificationError) Unwrap

func (e *VerificationError) Unwrap() error

type VerificationResult

type VerificationResult struct {
	// Verified indicates if the agent identity was successfully verified
	Verified bool `json:"verified"`

	// AgentName is the registered name of the agent
	AgentName string `json:"agentName,omitempty"`

	// VerifyURL is the public URL to verify this challenge result
	VerifyURL string `json:"verifyUrl,omitempty"`

	// Email is the email address associated with the agent
	Email string `json:"email,omitempty"`

	// Domain is the verified domain associated with this agent (if assigned)
	Domain string `json:"domain,omitempty"`

	// RegisteredSince is when this agent was first registered
	RegisteredSince *EpochTime `json:"registeredSince,omitempty"`

	// Error contains any error message if verification failed
	Error string `json:"error,omitempty"`

	// Challenge is the challenge code that was verified
	Challenge string `json:"challenge,omitempty"`
}

VerificationResult contains the result of a verification attempt

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL