Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

PEAC Go SDK

Go client library for PEAC protocol receipt issuance, verification, and policy evaluation.

Installation

go get github.com/peacprotocol/peac/sdks/go

Middleware

Framework-specific middleware packages are available as separate modules to avoid pulling unnecessary dependencies:

# Chi router
go get github.com/peacprotocol/peac/sdks/go/middleware/chi

# Gin framework
go get github.com/peacprotocol/peac/sdks/go/middleware/gin

Quick Start

Issuing Receipts

package main

import (
    "fmt"
    "log"

    peac "github.com/peacprotocol/peac/sdks/go"
)

func main() {
    // Create a signing key (in production, load from secure storage)
    signingKey, err := peac.GenerateSigningKey("my-key-id")
    if err != nil {
        log.Fatal(err)
    }

    result, err := peac.Issue(peac.IssueOptions{
        Issuer:     "https://publisher.example",
        Audience:   "https://agent.example",
        Amount:     1000,  // Amount in minor units (e.g., cents)
        Currency:   "USD",
        Rail:       "stripe",
        Reference:  "pi_abc123",
        SigningKey: signingKey,
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Receipt JWS: %s\n", result.JWS)
    fmt.Printf("Receipt ID: %s\n", result.ReceiptID)
}

Verifying Receipts

package main

import (
    "fmt"
    "log"
    "time"

    peac "github.com/peacprotocol/peac/sdks/go"
)

func main() {
    receiptJWS := "eyJhbGciOiJFZERTQSIsImtpZCI6Ii4uLiJ9..."

    result, err := peac.Verify(receiptJWS, peac.VerifyOptions{
        Issuer:   "https://publisher.example",
        Audience: "https://agent.example",
        MaxAge:   time.Hour,
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Receipt ID: %s\n", result.Claims.ReceiptID)
    fmt.Printf("Issued At: %d\n", result.Claims.IssuedAt)
    fmt.Printf("Purpose: %v\n", result.Claims.PurposeDeclared)
}

Evaluating Policies

package main

import (
    "fmt"
    "log"

    "github.com/peacprotocol/peac/sdks/go/policy"
)

func main() {
    doc := &policy.PolicyDocument{
        Version: policy.PolicyVersion,
        Rules: []policy.PolicyRule{
            {
                Name:     "allow-crawl",
                Decision: policy.Allow,
                Purpose:  policy.Purposes{policy.PurposeCrawl},
            },
            {
                Name:     "review-training",
                Decision: policy.Review,
                Purpose:  policy.Purposes{policy.PurposeTrain},
            },
        },
        Defaults: &policy.PolicyDefaults{
            Decision: policy.Deny,
            Reason:   "not explicitly allowed",
        },
    }

    result := policy.Evaluate(doc, &policy.EvaluationContext{
        Purpose: policy.PurposeCrawl,
    })

    fmt.Printf("Decision: %s\n", result.Decision)      // "allow"
    fmt.Printf("Matched: %s\n", result.MatchedRule)    // "allow-crawl"
}

Module Versioning

This SDK uses Go module versioning with path prefixes for nested modules. Tags follow the pattern sdks/go/vX.Y.Z:

Module Tag Pattern Example
Core SDK sdks/go/vX.Y.Z sdks/go/v0.9.29
Chi middleware sdks/go/middleware/chi/vX.Y.Z sdks/go/middleware/chi/v0.9.29
Gin middleware sdks/go/middleware/gin/vX.Y.Z sdks/go/middleware/gin/v0.9.29

This tagging strategy allows go get to resolve nested modules correctly from the monorepo.

Local Development

For local development, use Go workspaces:

cd sdks/go
go work sync
go test ./...

The workspace file (go.work) links all modules for seamless local development without modifying go.mod files.

Features

  • Ed25519 signature signing and verification
  • Receipt issuance with UUIDv7 receipt IDs (v0.9.29+)
  • Policy evaluation with first-match-wins semantics (v0.9.29+)
  • JWKS discovery and caching
  • Purpose claims support (v0.9.24+)
  • Agent identity attestation support (v0.9.25+)
  • Thread-safe JWKS cache with stale-while-revalidate
  • Comprehensive error types with retry hints
  • Evidence validation with DoS protection (v0.9.29+)

API Reference

Issue (v0.9.29+)

func Issue(opts IssueOptions) (*IssueResult, error)

Creates a signed PEAC receipt JWS.

IssueOptions

Field Type Description
Issuer string Issuer URL (required, must be https://)
Audience string Audience URL (required, must be https://)
Amount int64 Amount in minor units (required, >= 0)
Currency string ISO 4217 currency code (required, e.g., "USD")
Rail string Payment rail identifier (required)
Reference string Payment reference (required)
SigningKey *SigningKey Ed25519 signing key (required)
Subject string Subject URL (optional, must be https://)
Expiry int64 Unix timestamp for expiry (optional)
Env string Environment: "live" or "test" (default: "test")
Network string Payment network (optional)
Evidence any Additional evidence (optional, JSON-serializable)

URL Restrictions

All URL fields (Issuer, Audience, Subject) must:

  • Use the https:// scheme
  • Have a valid host
  • Not contain URL fragments (e.g., #section)
  • Not contain userinfo (e.g., user:pass@)

Evidence Structure

Evidence is placed in payment.evidence in the receipt claims (not at the top level):

// Evidence is nested under payment
opts := peac.IssueOptions{
    // ...
    Evidence: map[string]any{"custom": "data"},
}

// In the resulting receipt claims:
// claims.payment.evidence = {"custom": "data"}

Verify

func Verify(receiptJWS string, opts VerifyOptions) (*VerifyResult, error)

Verifies a PEAC receipt JWS and returns the verified claims.

VerifyOptions

Field Type Description
Issuer string Expected issuer (required)
Audience string Expected audience (required)
MaxAge time.Duration Maximum receipt age (default: 1 hour)
ClockSkew time.Duration Clock skew tolerance (default: 30s)
JWKSURL string Explicit JWKS URL (optional)
KeySet *jwks.KeySet Pre-loaded key set (optional)
JWKSCache *jwks.Cache JWKS cache instance (optional)
Context context.Context Request context

VerifyResult

type VerifyResult struct {
    Claims          *PEACReceiptClaims
    SubjectSnapshot *SubjectProfileSnapshot
    KeyID           string
    Algorithm       string
    Perf            *VerifyPerf
}

Policy Evaluation (v0.9.29+)

func Evaluate(policy *PolicyDocument, context *EvaluationContext) *EvaluationResult

Evaluates a policy against a context. Rules are evaluated in order; the first matching rule wins.

Nil Policy Behavior

If policy is nil, Evaluate returns a deny result:

result := policy.Evaluate(nil, ctx)
// result.Decision == policy.Deny
// result.Reason == policy.ReasonNilPolicy ("nil policy")
// result.IsDefault == true

Policy Constants

// Error codes for policy validation
const (
    ErrCodeInvalidPolicy        = "E_INVALID_POLICY"
    ErrCodeInvalidPolicyVersion = "E_INVALID_POLICY_VERSION"
    ErrCodeInvalidPolicyEnum    = "E_INVALID_POLICY_ENUM"
)

// Reason for nil policy evaluation
const ReasonNilPolicy = "nil policy"

JWKS Caching

For production use, create a shared JWKS cache:

cache := jwks.NewCache(jwks.CacheOptions{
    TTL:                  5 * time.Minute,
    StaleWhileRevalidate: true,
})

result, err := peac.Verify(receiptJWS, peac.VerifyOptions{
    Issuer:    "https://publisher.example",
    Audience:  "https://agent.example",
    JWKSCache: cache,
})

Error Handling

All errors are of type *PEACError with structured information:

result, err := peac.Verify(receiptJWS, opts)
if err != nil {
    if peacErr, ok := err.(*peac.PEACError); ok {
        fmt.Printf("Error Code: %s\n", peacErr.Code)
        fmt.Printf("HTTP Status: %d\n", peacErr.HTTPStatus())
        fmt.Printf("Retryable: %v\n", peacErr.IsRetryable())
    }
}

Error Codes

Code HTTP Description
E_INVALID_SIGNATURE 400 Signature verification failed
E_INVALID_FORMAT 400 Invalid JWS format
E_EXPIRED 401 Receipt has expired
E_NOT_YET_VALID 401 Receipt not yet valid
E_INVALID_ISSUER 400 Issuer mismatch
E_INVALID_AUDIENCE 400 Audience mismatch
E_JWKS_FETCH_FAILED 503 Failed to fetch JWKS
E_KEY_NOT_FOUND 400 Key ID not in JWKS

Issue Error Codes (v0.9.29+)

Code Description
E_INVALID_ISSUER Invalid issuer URL
E_INVALID_AUDIENCE Invalid audience URL
E_INVALID_SUBJECT Invalid subject URL
E_INVALID_CURRENCY Invalid currency code
E_INVALID_AMOUNT Invalid amount (negative)
E_INVALID_EXPIRY Invalid expiry (negative)
E_INVALID_ENV Invalid env (must be "live"/"test")
E_INVALID_RAIL Missing payment rail
E_INVALID_REFERENCE Missing payment reference
E_INVALID_EVIDENCE Evidence validation failed
E_MISSING_SIGNING_KEY No signing key provided

Identity Error Codes (v0.9.25+)

Code HTTP Description
E_IDENTITY_MISSING 401 No identity attestation
E_IDENTITY_SIG_INVALID 401 Identity signature invalid
E_IDENTITY_EXPIRED 401 Identity attestation expired
E_IDENTITY_KEY_UNKNOWN 401 Identity key not found

Claims Structure

type PEACReceiptClaims struct {
    // Standard JWT claims
    Issuer    string   `json:"iss"`
    Subject   string   `json:"sub,omitempty"`
    Audience  []string `json:"aud,omitempty"`
    IssuedAt  int64    `json:"iat"`
    ExpiresAt int64    `json:"exp,omitempty"`
    JWTID     string   `json:"jti"`

    // PEAC claims
    ReceiptID       string   `json:"receipt_id"`
    PurposeDeclared []string `json:"purpose_declared,omitempty"`
    PurposeEnforced string   `json:"purpose_enforced,omitempty"`
    Decision        string   `json:"decision,omitempty"`

    // Evidence
    Payment      *PaymentEvidence `json:"payment,omitempty"`
    Attestations []Attestation    `json:"attestations,omitempty"`
}

Agent Identity (v0.9.25+)

The SDK supports agent identity attestations:

type AgentIdentityEvidence struct {
    AgentID         string      `json:"agent_id"`
    ControlType     string      `json:"control_type"`  // "operator" or "user-delegated"
    Capabilities    []string    `json:"capabilities,omitempty"`
    DelegationChain []string    `json:"delegation_chain,omitempty"`
    Proof           *AgentProof `json:"proof,omitempty"`
}

Development

Local Verification

Run all CI checks locally before pushing:

./scripts/verify.sh

This runs: format check, build, test, race detection, middleware tests, and fuzz testing.

Versioning

The Go SDK uses module-path versioning. Each module has its own tag:

Module Tag Pattern
github.com/peacprotocol/peac/sdks/go sdks/go/vX.Y.Z
github.com/peacprotocol/peac/sdks/go/middleware/chi sdks/go/middleware/chi/vX.Y.Z
github.com/peacprotocol/peac/sdks/go/middleware/gin sdks/go/middleware/gin/vX.Y.Z

For example, v0.9.29 would have tags:

  • sdks/go/v0.9.29
  • sdks/go/middleware/chi/v0.9.29
  • sdks/go/middleware/gin/v0.9.29

Requirements

  • Go 1.26 or later
  • golang.org/x/crypto for Ed25519

License

Apache-2.0 - see LICENSE