Help us shape the future of Go! Take the 2026 Go Developer Survey

server

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT, Apache-2.0, MIT Imports: 19 Imported by: 0

Documentation

Overview

Package server provides the generic challenge-or-verify flow for Payment authentication on the server side.

Examples import this package by its bare name. The Tempo charge package uses the alias `charge`, so there is no conflict.

Package server provides the server-side MPP payment handler.

It implements the HTTP 402 challenge/credential flow: when a request arrives without payment credentials, it returns a 402 with a WWW-Authenticate challenge. When valid credentials are provided, it verifies them and returns a receipt.

Index

Examples

Constants

View Source
const MaxRequestBodyBytes = 4 << 20

MaxRequestBodyBytes bounds the body buffered for payment digest verification. Configure the HTTP server's read timeout separately to bound slow uploads.

Variables

This section is empty.

Functions

func ChallengeSerializationError added in v0.5.1

func ChallengeSerializationError(challenge *mpp.Challenge, realm string, err error) error

ChallengeSerializationError classifies err, a failure to serialize challenge as a WWW-Authenticate value for realm. Challenges come from server configuration, so the failure is a server error (WritePaymentResponse reports it as a 500 internal payment error) unless the request-derived resource or query bound into the challenge's _mppx_scope made the header too large: then the request URI is at fault and the result is a 414 URI Too Long PaymentError.

func ChargeMiddleware

func ChargeMiddleware(m *Mpp, params ChargeParams) func(http.Handler) http.Handler

ChargeMiddleware creates an http.Handler middleware for the charge intent.

It runs Mpp.ChargeRequest for each request with the provided ChargeParams, which reads the incoming Authorization or Payment-Authorization header, body and route scope, writes the 402 challenge or problem response when payment is required or rejected, and stores the verified Credential and Receipt in the request context on success. It behaves as ComposeMiddleware with a single offer.

func ComposeMiddleware

func ComposeMiddleware(configs ...ComposeConfig) func(http.Handler) http.Handler

ComposeMiddleware creates an http.Handler middleware that supports multiple payment methods on a single route.

When no credential is present, it fans out to every configured method and returns a merged 402 response with one WWW-Authenticate header per method. When a credential is present, it dispatches to the matching method by comparing the credential's echoed method, intent, and canonical request.

All configs must share the same realm. ComposeMiddleware panics if configs is empty, any Mpp is nil, or realms differ.

func ContextWithPayment

func ContextWithPayment(ctx context.Context, credential *mpp.Credential, receipt *mpp.Receipt) context.Context

ContextWithPayment stores the verified payment objects on a request context.

func CredentialFromContext

func CredentialFromContext(ctx context.Context) *mpp.Credential

CredentialFromContext extracts the Credential from the request context.

func DeferPaymentReceipt added in v0.4.0

func DeferPaymentReceipt(w http.ResponseWriter, receipt *mpp.Receipt) (http.ResponseWriter, func())

DeferPaymentReceipt returns a writer that adds the receipt only when a 2xx response status is committed. Call complete after the handler returns to commit an otherwise empty successful response. A receipt mpp.FormatReceiptWithError rejects is left out.

func DetectRealm

func DetectRealm() string

DetectRealm auto-detects the server realm from environment variables. It checks MPP_REALM, FLY_APP_NAME, HEROKU_APP_NAME, RAILWAY_PUBLIC_DOMAIN, RENDER_EXTERNAL_HOSTNAME, VERCEL_URL, and WEBSITE_HOSTNAME in that order. Returns "MPP Payment" if none are set. HOST and HOSTNAME are not read: container runtimes set HOSTNAME per replica, and the realm is bound into the challenge ID, so replicas would reject each other's credentials.

func DetectSecretKey

func DetectSecretKey() (string, error)

DetectSecretKey reads the MPP_SECRET_KEY environment variable. Returns an error if it is not set.

func IsPaymentOutcomeUnknown added in v0.5.1

func IsPaymentOutcomeUnknown(err error) bool

IsPaymentOutcomeUnknown reports whether err is an mpp.ErrPaymentOutcomeUnknown problem. Writers send it without retry challenges, since the credential it answers may already have settled.

func ReadRequestBody added in v0.2.0

func ReadRequestBody(r *http.Request) ([]byte, error)

ReadRequestBody reads and restores r.Body so middleware can verify body digests without consuming the body before the protected handler runs.

func ReceiptFromContext

func ReceiptFromContext(ctx context.Context) *mpp.Receipt

ReceiptFromContext extracts the Receipt from the request context.

func RequestBodyError added in v0.4.0

func RequestBodyError(err error) *mpp.PaymentError

RequestBodyError maps oversized bodies to HTTP 413 and other read failures to 400.

func ScopeFromHTTPRequest added in v0.2.0

func ScopeFromHTTPRequest(r *http.Request, route string) map[string]string

ScopeFromHTTPRequest returns the framework scope bound into charge requests.

func WritePaymentResponse added in v0.5.1

func WritePaymentResponse(w http.ResponseWriter, err error, challenges ...*mpp.Challenge)

WritePaymentResponse writes the payment response for err and challenges, each challenge as one WWW-Authenticate field value, in order, with its own Realm (the realm its ID binds):

  • err nil: the initial 402 Payment Required offering challenges, with Cache-Control: no-store. Without challenges it is a 500 internal payment error, since the server issued no offers.
  • err non-nil: err as RFC 9457 application/problem+json. A PaymentError is written with its own status; any other error is a 500 internal payment error whose text never reaches the client. Challenges are retry offers, omitted for ErrPaymentOutcomeUnknown, since the credential it answers may already have settled.

Every challenge is serialized before any header is written, so a failure cannot leave a partial set of offers; it is classified by ChallengeSerializationError (414 or 500). Nil challenges are skipped.

Types

type BroadcastFunc added in v0.4.0

type BroadcastFunc func(
	ctx context.Context,
	credential *mpp.Credential,
	request map[string]any,
) (*mpp.Receipt, error)

BroadcastFunc finalizes a validated credential and may mutate payment state.

type BroadcastingIntent added in v0.4.0

type BroadcastingIntent interface {
	ValidatingIntent
	Broadcast(ctx context.Context, credential *mpp.Credential, request map[string]any) (*mpp.Receipt, error)
}

BroadcastingIntent supports a split validation and settlement lifecycle.

Broadcast must revalidate the credential before performing its terminal payment operation. Verify remains the compatibility path for callers that do not use the split lifecycle explicitly.

type ChargeOffersBuilder added in v0.4.0

type ChargeOffersBuilder interface {
	BuildChargeRequests(params ChargeParams) ([]map[string]any, error)
}

ChargeOffersBuilder lets a method issue several ordered charge offers for one charge, for example one per accepted currency. Charge issues one Challenge per returned request and verifies a Credential against the offer it echoes. When a method implements both interfaces, BuildChargeRequests takes precedence for Charge.

type ChargeParams

type ChargeParams struct {
	// Authorization is the incoming Authorization header value. ChargeRequest
	// sets it from the request.
	Authorization string
	// PaymentAuthorization is the incoming Payment-Authorization header value.
	// Used when the handler was created with WithRequiresAuth(true).
	// ChargeRequest sets it from the request.
	PaymentAuthorization string
	// Amount is the human-readable charge amount.
	Amount string
	// Currency overrides the method's default currency.
	Currency string
	// Recipient overrides the method's default recipient.
	Recipient string
	// ExternalID is copied into the request and echoed back in the Receipt.
	ExternalID string
	// Body is the actual request body to bind via the challenge digest. It is
	// digested as VerifyParams.Body is. ChargeRequest sets it from a
	// non-empty request body.
	Body any
	// Expires overrides the default Challenge expiry.
	Expires string
	// Description is exposed in the server-generated Challenge.
	Description string
	// Splits adds Tempo split-payment transfers under methodDetails.splits.
	Splits []tempo.SplitParams
	// FeePayer requests the sponsored Tempo flow when the method supports it.
	FeePayer bool
	// FeePayerURL points at a remote fee-payer signer.
	FeePayerURL string
	// SupportedModes advertises the Tempo submission modes allowed for this challenge.
	SupportedModes []tempo.ChargeMode
	// ChainID overrides the method's default Tempo chain ID.
	ChainID int
	// Meta stores opaque Challenge metadata.
	Meta map[string]string
	// MppxScope binds framework route/resource/query context into the
	// request. When empty, ChargeRequest and the middlewares set it from the
	// request (see ScopeFromHTTPRequest); a non-empty value is kept as is.
	MppxScope map[string]string
}

ChargeParams contains the parameters for a charge operation.

Authorization, PaymentAuthorization, Body and MppxScope describe the incoming request rather than the price. ChargeRequest, ChargeMiddleware and the framework adapters set them from the HTTP request; set them yourself only when calling Charge for another transport.

type ChargeRequestBuilder

type ChargeRequestBuilder interface {
	BuildChargeRequest(params ChargeParams) (map[string]any, error)
}

ChargeRequestBuilder lets a method provide a canonical request shape for the generic charge helper. Tempo uses this to normalize human amounts and nest method-specific fields under methodDetails.

type ChargeResult

type ChargeResult struct {
	// Challenge is returned when the client still needs to satisfy the payment.
	// With several offers it is the offer the submitted Credential echoed, or
	// the first offer otherwise.
	Challenge *mpp.Challenge
	// Challenges lists every offered Challenge in presentation order whenever
	// Challenge is set. It contains a single element for single-offer methods.
	Challenges []*mpp.Challenge
	// Credential is the verified client credential on success.
	Credential *mpp.Credential
	// Receipt acknowledges a successfully verified payment.
	Receipt *mpp.Receipt
	// contains filtered or unexported fields
}

ChargeResult is either a Challenge or a verified (Credential, Receipt) pair.

func Compose added in v0.6.0

func Compose(ctx context.Context, authorization string, offers ...Offer) (*ChargeResult, error)

Compose runs a composed charge for transports other than HTTP, such as MCP, that carry the Payment credential themselves. authorization is the credential formatted as an Authorization value ("Payment <base64url>"), or empty when the client sent none.

Without a credential it returns every offered Challenge in offer order. With one it verifies it against the offer whose method, intent and request it echoes, like ComposeMiddleware. A malformed credential returns every Challenge its method and intent offer (or every Challenge) with a malformed-credential error. Each offer's ChargeParams are used as given: set Body or MppxScope there to bind them. The result is nil only when err is non-nil and no Challenge could be issued.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/tempoxyz/mpp-go/pkg/mpp"
	"github.com/tempoxyz/mpp-go/pkg/server"
)

// exampleMethod accepts every Credential; real servers use a payment method
// such as Tempo's.
type exampleMethod struct{ name string }

func (m exampleMethod) Name() string { return m.name }

func (m exampleMethod) Intents() map[string]server.Intent {
	return map[string]server.Intent{mpp.IntentCharge: exampleIntent{method: m.name}}
}

type exampleIntent struct{ method string }

func (exampleIntent) Name() string { return mpp.IntentCharge }

func (i exampleIntent) Verify(context.Context, *mpp.Credential, map[string]any) (*mpp.Receipt, error) {
	return mpp.Success(i.method, "0xreceipt"), nil
}

const exampleSecret = "example-secret-minimum-32-byte-secret"

func exampleCredential(challenge *mpp.Challenge) string {
	authorization, err := mpp.FormatCredential(challenge.NewCredential(map[string]any{"type": "hash", "hash": "0xabc"}))
	if err != nil {
		log.Fatal(err)
	}
	return authorization
}

func main() {
	tempo, _ := server.New(exampleMethod{name: "tempo"}, "api.example.com", exampleSecret)
	stripe, _ := server.New(exampleMethod{name: "stripe"}, "api.example.com", exampleSecret)
	offers := []server.Offer{
		{Mpp: tempo, Params: server.ChargeParams{Amount: "0.01"}},
		{Mpp: stripe, Params: server.ChargeParams{Amount: "0.01"}},
	}

	// Without a Credential, every offer is returned.
	offered, err := server.Compose(context.Background(), "", offers...)
	if err != nil {
		log.Fatal(err)
	}
	for _, challenge := range offered.Challenges {
		fmt.Println("offer:", challenge.Method)
	}

	// A Credential is verified against the offer it echoes.
	paid, err := server.Compose(context.Background(), exampleCredential(offered.Challenges[1]), offers...)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("paid with:", paid.Receipt.Method)
}
Output:
offer: tempo
offer: stripe
paid with: stripe

func ComposeRequest added in v0.6.0

func ComposeRequest(r *http.Request, offers ...Offer) (*ChargeResult, error)

ComposeRequest runs a composed charge for an HTTP request, like ComposeMiddleware does for each request, reading the Payment credential, body and route scope from r (see Mpp.ChargeRequest). An offer whose Params.MppxScope is set keeps that scope.

It always returns a non-nil result. Write it with its ServeHTTP method before anything else, as for Mpp.ChargeRequest: that writes the 402 or problem response, or, for a verified payment, sets the Payment-Receipt header for the resource the handler then writes.

func (*ChargeResult) IsChallenge

func (r *ChargeResult) IsChallenge() bool

IsChallenge returns true if the result is a 402 challenge. A nil result is not a challenge.

func (*ChargeResult) ServeHTTP added in v0.6.0

func (r *ChargeResult) ServeHTTP(w http.ResponseWriter, _ *http.Request)

ServeHTTP writes the payment response for the result. Call it for every result, before writing the resource, so a paid response carries its Payment-Receipt:

res, err := payment.ChargeRequest(r, params)
res.ServeHTTP(w, r)
if err != nil || res.IsChallenge() {
	return // the 402 or problem response is written
}
// The payment is verified: write the resource.

For a Challenge, or an error returned with the result, it writes them as WritePaymentResponse does: the 402 offering every Challenge, or the problem details with retry Challenges. For a verified payment it only sets the Payment-Receipt and Cache-Control: private headers, leaving the status and body to the caller. A nil result writes a 500 internal payment error.

type ComposeConfig

type ComposeConfig struct {
	Mpp    *Mpp
	Params ChargeParams
}

ComposeConfig pairs an Mpp instance with the ChargeParams for one payment method. Pass one or more of these to ComposeMiddleware to advertise multiple payment options on a single route. New code can use its alias Offer.

type Events added in v0.6.0

type Events struct {
	// ChallengeCreated is called for every Challenge returned to the client,
	// whether as the initial offer or as a retry offer after a failure. It
	// is called only once the whole set of Challenges for the response is
	// issued and can be serialized, never for an offer that is not returned.
	ChallengeCreated func(ctx context.Context, challenge *mpp.Challenge)
	// PaymentSucceeded is called after a Credential is verified and settled.
	PaymentSucceeded func(ctx context.Context, credential *mpp.Credential, receipt *mpp.Receipt)
	// PaymentFailed is called when a submitted Credential is rejected or
	// cannot be settled. credential is nil when it could not be parsed.
	PaymentFailed func(ctx context.Context, credential *mpp.Credential, err error)
}

Events are hooks a handler calls as payments progress, like mppx's onChallengeCreated, onPaymentSuccess and onPaymentFailed. Every hook is optional and runs synchronously on the request's goroutine, so keep it fast and do not block in it. Hooks observe the flow; they cannot change its outcome: a hook that panics is recovered and the panic is ignored, so a request whose payment settled is still served.

type Intent

type Intent interface {
	Name() string
	Verify(ctx context.Context, credential *mpp.Credential, request map[string]any) (*mpp.Receipt, error)
}

Intent is the interface for payment intents (server-side verification).

func NewIntent added in v0.4.0

func NewIntent(name string, hooks IntentHooks) (Intent, error)

NewIntent builds an Intent from either Verify or Validate and Broadcast hooks.

type IntentHooks added in v0.4.0

type IntentHooks struct {
	// Verify configures the legacy combined verification path.
	Verify VerifyFunc
	// Validate configures the advisory phase of a split verification path.
	Validate ValidateFunc
	// Broadcast configures the settlement phase of a split verification path.
	Broadcast BroadcastFunc
}

IntentHooks configures either a legacy Verify hook or split Validate and Broadcast hooks.

type Method

type Method interface {
	Name() string
	Intents() map[string]Intent
}

Method is the interface for server-side payment methods.

type Mpp

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

Mpp is the server-side payment handler.

func New

func New(method Method, realm, secretKey string, opts ...Option) (*Mpp, error)

New creates an Mpp instance.

An empty secretKey defaults to DetectSecretKey (MPP_SECRET_KEY). An empty realm stays empty, as before, because the realm is bound into every Challenge and replicas with different realms reject each other's Challenges; pass DetectRealm() for the environment's realm. New returns an error when method is nil, when secretKey is empty and MPP_SECRET_KEY is unset, or when the secret key is shorter than 32 bytes, since a weak secret yields forgeable, HMAC-bound challenge IDs.

func (*Mpp) BroadcastCredential added in v0.4.0

func (m *Mpp) BroadcastCredential(ctx context.Context, credential *mpp.Credential) (*mpp.Receipt, error)

BroadcastCredential validates and settles an issued credential.

func (*Mpp) Charge

func (m *Mpp) Charge(ctx context.Context, params ChargeParams) (*ChargeResult, error)

Charge handles a charge intent with human-readable amounts, for any transport: params carries the Credential (Authorization or PaymentAuthorization) and the request Body and MppxScope to bind. For an HTTP request, ChargeRequest reads those from the request instead.

Methods implementing ChargeOffersBuilder may offer several requests; Charge then returns one Challenge per offer and verifies a Credential against the offer it echoes. The result is nil only when err is non-nil and no Challenge could be issued.

func (*Mpp) ChargeRequest added in v0.6.0

func (m *Mpp) ChargeRequest(r *http.Request, params ChargeParams) (*ChargeResult, error)

ChargeRequest runs the charge for an HTTP request, as ChargeMiddleware does for each request. It reads the Payment credential from the Authorization header (Payment-Authorization for a handler created WithRequiresAuth), the body (restoring r.Body for the handler) and, unless params.MppxScope is set, the route scope (see ScopeFromHTTPRequest), overriding the matching params fields.

It always returns a non-nil result. Write it with its ServeHTTP method before anything else: when err is non-nil or the result IsChallenge, that writes the 402 or problem response. Otherwise the payment is verified, the result carries its Credential and Receipt, and ServeHTTP sets the Payment-Receipt and Cache-Control: private headers for the resource the handler then writes.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"

	"github.com/tempoxyz/mpp-go/pkg/mpp"
	"github.com/tempoxyz/mpp-go/pkg/server"
)

// exampleMethod accepts every Credential; real servers use a payment method
// such as Tempo's.
type exampleMethod struct{ name string }

func (m exampleMethod) Name() string { return m.name }

func (m exampleMethod) Intents() map[string]server.Intent {
	return map[string]server.Intent{mpp.IntentCharge: exampleIntent{method: m.name}}
}

type exampleIntent struct{ method string }

func (exampleIntent) Name() string { return mpp.IntentCharge }

func (i exampleIntent) Verify(context.Context, *mpp.Credential, map[string]any) (*mpp.Receipt, error) {
	return mpp.Success(i.method, "0xreceipt"), nil
}

const exampleSecret = "example-secret-minimum-32-byte-secret"

func exampleCredential(challenge *mpp.Challenge) string {
	authorization, err := mpp.FormatCredential(challenge.NewCredential(map[string]any{"type": "hash", "hash": "0xabc"}))
	if err != nil {
		log.Fatal(err)
	}
	return authorization
}

func main() {
	payment, err := server.New(exampleMethod{name: "tempo"}, "api.example.com", exampleSecret)
	if err != nil {
		log.Fatal(err)
	}
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		res, err := payment.ChargeRequest(r, server.ChargeParams{Amount: "0.01"})
		// Writes the 402 with the Challenge or the problem details, or sets
		// the Payment-Receipt header for a verified payment.
		res.ServeHTTP(w, r)
		if err != nil || res.IsChallenge() {
			return
		}
		fmt.Fprintln(w, "paid:", res.Receipt.Reference)
	})

	unpaid := httptest.NewRecorder()
	handler.ServeHTTP(unpaid, httptest.NewRequest(http.MethodGet, "/premium", nil))
	fmt.Println(unpaid.Code)

	challenge, _ := mpp.ParseChallenge(unpaid.Header().Get(mpp.HeaderWWWAuthenticate))
	r := httptest.NewRequest(http.MethodGet, "/premium", nil)
	r.Header.Set(mpp.HeaderAuthorization, exampleCredential(challenge))
	paid := httptest.NewRecorder()
	handler.ServeHTTP(paid, r)
	fmt.Print(paid.Body.String())
	receipt, _ := mpp.ParseReceipt(paid.Header().Get(mpp.HeaderPaymentReceipt))
	fmt.Println("receipt:", receipt.Reference)
}
Output:
402
paid: 0xreceipt
receipt: 0xreceipt

func (*Mpp) CredentialHeader added in v0.4.0

func (m *Mpp) CredentialHeader() string

CredentialHeader returns the HTTP field this handler expects for Payment credentials.

func (*Mpp) Realm

func (m *Mpp) Realm() string

Realm returns the WWW-Authenticate realm used by this payment handler.

func (*Mpp) RequiresAuth added in v0.4.0

func (m *Mpp) RequiresAuth() bool

RequiresAuth reports whether Payment credentials use Payment-Authorization.

func (*Mpp) ValidateCredential added in v0.4.0

func (m *Mpp) ValidateCredential(ctx context.Context, credential *mpp.Credential) (*Validation, error)

ValidateCredential validates an issued credential without consuming or broadcasting it.

func (*Mpp) VerifyCredential added in v0.4.0

func (m *Mpp) VerifyCredential(ctx context.Context, credential *mpp.Credential) (*mpp.Receipt, error)

VerifyCredential is a backwards-compatible alias for BroadcastCredential.

type Offer added in v0.6.0

type Offer = ComposeConfig

Offer pairs an Mpp instance with the ChargeParams of one payment option. Pass several to Compose, ComposeRequest or ComposeMiddleware to offer multiple payment options for one resource. It is the preferred name for ComposeConfig.

type Option added in v0.4.0

type Option func(*Mpp)

Option configures an Mpp instance.

func WithChallengeNonce added in v0.6.0

func WithChallengeNonce(enabled bool) Option

WithChallengeNonce makes every Challenge this handler issues unique by binding a random _mpp_nonce into its opaque metadata, so identical charges never share a Challenge ID. Enable it only after every replica verifies nonce-bearing Challenges; verification accepts Challenges with and without a nonce either way.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/tempoxyz/mpp-go/pkg/mpp"
	"github.com/tempoxyz/mpp-go/pkg/server"
)

// exampleMethod accepts every Credential; real servers use a payment method
// such as Tempo's.
type exampleMethod struct{ name string }

func (m exampleMethod) Name() string { return m.name }

func (m exampleMethod) Intents() map[string]server.Intent {
	return map[string]server.Intent{mpp.IntentCharge: exampleIntent{method: m.name}}
}

type exampleIntent struct{ method string }

func (exampleIntent) Name() string { return mpp.IntentCharge }

func (i exampleIntent) Verify(context.Context, *mpp.Credential, map[string]any) (*mpp.Receipt, error) {
	return mpp.Success(i.method, "0xreceipt"), nil
}

const exampleSecret = "example-secret-minimum-32-byte-secret"

func main() {
	payment, _ := server.New(exampleMethod{name: "tempo"}, "api.example.com", exampleSecret,
		server.WithChallengeNonce(true),
	)
	params := server.ChargeParams{Amount: "0.01", Expires: mpp.ExpiresIn(5 * time.Minute)}
	first, _ := payment.Charge(context.Background(), params)
	second, _ := payment.Charge(context.Background(), params)
	fmt.Println(first.Challenge.ID != second.Challenge.ID)
}
Output:
true

func WithEvents added in v0.6.0

func WithEvents(events Events) Option

WithEvents sets the hooks the handler calls as payments progress.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/tempoxyz/mpp-go/pkg/mpp"
	"github.com/tempoxyz/mpp-go/pkg/server"
)

// exampleMethod accepts every Credential; real servers use a payment method
// such as Tempo's.
type exampleMethod struct{ name string }

func (m exampleMethod) Name() string { return m.name }

func (m exampleMethod) Intents() map[string]server.Intent {
	return map[string]server.Intent{mpp.IntentCharge: exampleIntent{method: m.name}}
}

type exampleIntent struct{ method string }

func (exampleIntent) Name() string { return mpp.IntentCharge }

func (i exampleIntent) Verify(context.Context, *mpp.Credential, map[string]any) (*mpp.Receipt, error) {
	return mpp.Success(i.method, "0xreceipt"), nil
}

const exampleSecret = "example-secret-minimum-32-byte-secret"

func exampleCredential(challenge *mpp.Challenge) string {
	authorization, err := mpp.FormatCredential(challenge.NewCredential(map[string]any{"type": "hash", "hash": "0xabc"}))
	if err != nil {
		log.Fatal(err)
	}
	return authorization
}

func main() {
	payment, _ := server.New(exampleMethod{name: "tempo"}, "api.example.com", exampleSecret,
		server.WithEvents(server.Events{
			ChallengeCreated: func(_ context.Context, challenge *mpp.Challenge) {
				fmt.Println("challenge created for", challenge.Method)
			},
			PaymentSucceeded: func(_ context.Context, _ *mpp.Credential, receipt *mpp.Receipt) {
				fmt.Println("payment succeeded:", receipt.Reference)
			},
			PaymentFailed: func(_ context.Context, _ *mpp.Credential, err error) {
				fmt.Println("payment failed")
			},
		}),
	)
	res, _ := payment.Charge(context.Background(), server.ChargeParams{Amount: "0.01"})
	_, _ = payment.Charge(context.Background(), server.ChargeParams{
		Amount:        "0.01",
		Authorization: exampleCredential(res.Challenge),
	})
}
Output:
challenge created for tempo
payment succeeded: 0xreceipt

func WithRequiresAuth added in v0.4.0

func WithRequiresAuth(enabled bool) Option

WithRequiresAuth advertises header="Payment-Authorization" so Payment credentials leave Authorization available for application authentication.

type ValidateFunc added in v0.4.0

type ValidateFunc func(
	ctx context.Context,
	credential *mpp.Credential,
	request map[string]any,
) (*Validation, error)

ValidateFunc performs an advisory credential check without mutating payment state.

type ValidatingIntent added in v0.4.0

type ValidatingIntent interface {
	Intent
	Validate(ctx context.Context, credential *mpp.Credential, request map[string]any) (*Validation, error)
}

ValidatingIntent supports non-mutating credential validation.

Validate must not consume replay state, sign transactions, broadcast, or otherwise mutate payment state. Callers must use BroadcastCredential to accept a payment after validation.

type Validation added in v0.4.0

type Validation struct {
	// Challenge is the server-issued challenge echoed by the credential.
	Challenge mpp.ChallengeEcho
	// Credential is the accepted credential.
	Credential *mpp.Credential
	// Details contains method-specific validation metadata.
	Details map[string]any
	// Intent is the validated payment intent.
	Intent string
	// Method is the validated payment method.
	Method string
	// Request is the challenge request bound to the credential.
	Request map[string]any
	// Source is the optional payer identity.
	Source string
}

Validation describes a credential accepted without consuming or broadcasting it.

type VerifyFunc added in v0.4.0

type VerifyFunc func(
	ctx context.Context,
	credential *mpp.Credential,
	request map[string]any,
) (*mpp.Receipt, error)

VerifyFunc is the legacy combined credential verification hook.

type VerifyParams

type VerifyParams struct {
	// EnableChallengeNonce opts into unique issuance after every replica has
	// nonce-aware verification. The default preserves legacy issuance for
	// rollout. Mpp.Charge sets it for a handler created WithChallengeNonce.
	EnableChallengeNonce bool
	// Authorization is the incoming Authorization header value.
	Authorization string
	// Intent verifies Credentials for this request.
	Intent Intent
	// Request is the canonical request shape the Challenge binds to.
	Request map[string]any
	// Body is the actual request body bytes/string/object the digest binds to.
	// When set, issued challenges include a body digest and submitted
	// credentials must echo a digest matching this value (see
	// mpp.ComputeBodyDigest). A []byte or string, or a named type of either,
	// is digested as is; any other value as its json.Marshal output. An
	// io.Reader or a value json.Marshal cannot encode is an error.
	Body any
	// Realm is the expected server realm.
	Realm string
	// SecretKey signs and verifies Challenge IDs.
	SecretKey string
	// Method is the payment method token, for example "tempo".
	Method string
	// Description is copied into a newly issued Challenge.
	Description string
	// Meta stores opaque Challenge metadata.
	Meta map[string]string
	// Expires overrides the default Challenge expiry.
	Expires string
	// Header selects the HTTP field for the Payment credential. Empty defaults
	// to Authorization. Payment-Authorization is advertised when set.
	Header string
}

VerifyParams contains the parameters for VerifyOrChallenge.

type VerifyResult

type VerifyResult struct {
	// Challenge is returned when the server needs the client to pay first.
	Challenge *mpp.Challenge
	// Credential is the parsed client credential on success.
	Credential *mpp.Credential
	// Receipt acknowledges successful verification.
	Receipt *mpp.Receipt
}

VerifyResult is either a Challenge or a verified (Credential, Receipt) pair.

func VerifyOrChallenge

func VerifyOrChallenge(ctx context.Context, params VerifyParams) (*VerifyResult, error)

VerifyOrChallenge checks for a valid payment credential or generates a new challenge. Returns a Challenge if payment is required, or (Credential, Receipt) if verified.

Logic:

  1. If no authorization header, create new challenge
  2. Extract "Payment" scheme from Authorization header
  3. Parse credential
  4. Recompute expected challenge ID from echoed parameters via HMAC
  5. Constant-time compare IDs
  6. Verify echoed fields match (realm, method, intent name, request)
  7. Check expiry
  8. Verify body digest, if present
  9. Validate then broadcast split intents, or call legacy intent.Verify
  10. Return result

func (*VerifyResult) IsChallenge added in v0.6.0

func (r *VerifyResult) IsChallenge() bool

IsChallenge reports whether the result is a Challenge the client must satisfy, rather than a verified payment. A nil result is not a challenge.

Directories

Path Synopsis
Package echoadapter provides Echo middleware for MPP-protected routes.
Package echoadapter provides Echo middleware for MPP-protected routes.
Package fiberadapter provides Fiber middleware for MPP-protected routes.
Package fiberadapter provides Fiber middleware for MPP-protected routes.
Package ginadapter provides Gin middleware for MPP-protected routes.
Package ginadapter provides Gin middleware for MPP-protected routes.

Jump to

Keyboard shortcuts

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