Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/TecharoHq/Anubis/llms.txt

Use this file to discover all available pages before exploring further.

Anubis supports custom challenge types through the challenge.Impl interface. This allows you to create specialized bot detection mechanisms beyond the built-in proof-of-work, meta-refresh, and Preact challenges.

Challenge Interface

All challenge implementations must satisfy the challenge.Impl interface:
type Impl interface {
	// Setup registers any additional routes with the Impl for assets or API routes.
	Setup(mux *http.ServeMux)

	// Issue a new challenge to the user, called by the Anubis.
	Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)

	// Validate a challenge, making sure that it passes muster.
	Validate(r *http.Request, lg *slog.Logger, in *ValidateInput) error
}
Source: lib/challenge/interface.go:59-68

Input Structures

IssueInput

Provided when issuing a new challenge:
type IssueInput struct {
	Impressum *config.Impressum
	Rule      *policy.Bot
	Challenge *Challenge
	OGTags    map[string]string
	Store     store.Interface
}

ValidateInput

Provided when validating a challenge response:
type ValidateInput struct {
	Rule      *policy.Bot
	Challenge *Challenge
	Store     store.Interface
}

Challenge Metadata

type Challenge struct {
	IssuedAt       time.Time         `json:"issuedAt"`
	Metadata       map[string]string `json:"metadata"`
	ID             string            `json:"id"`
	Method         string            `json:"method"`
	RandomData     string            `json:"randomData"`
	PolicyRuleHash string            `json:"policyRuleHash,omitempty"`
	Difficulty     int               `json:"difficulty,omitempty"`
	Spent          bool              `json:"spent"`
}
Source: lib/challenge/challenge.go:6-15

Implementation Example: Proof-of-Work

Here’s how the built-in proof-of-work challenge is implemented:
package proofofwork

import (
	"crypto/subtle"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"strings"

	chall "github.com/TecharoHQ/anubis/lib/challenge"
	"github.com/TecharoHQ/anubis/lib/localization"
	"github.com/a-h/templ"
)

func init() {
	chall.Register("fast", &Impl{Algorithm: "fast")
	chall.Register("slow", &Impl{Algorithm: "slow")
}

type Impl struct {
	Algorithm string
}

func (i *Impl) Setup(mux *http.ServeMux) {}

func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
	loc := localization.GetLocalizer(r)
	return page(loc), nil
}

func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
	rule := in.Rule
	challenge := in.Challenge.RandomData

	nonceStr := r.FormValue("nonce")
	if nonceStr == "" {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
	}

	nonce, err := strconv.Atoi(nonceStr)
	if err != nil {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
	}

	response := r.FormValue("response")
	if response == "" {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
	}

	calcString := fmt.Sprintf("%s%d", challenge, nonce)
	calculated := internal.SHA256sum(calcString)

	if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
	}

	// compare the leading zeroes
	if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response))
	}

	return nil
}
Source: lib/challenge/proofofwork/proofofwork.go

Implementation Example: Meta Refresh

The meta-refresh challenge validates timing constraints:
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
	wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 800 * time.Millisecond)

	if time.Now().Before(wantTime) {
		return challenge.NewError("validate", "insufficient time", 
			fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339)))
	}

	gotChallenge := r.FormValue("challenge")

	if subtle.ConstantTimeCompare([]byte(in.Challenge.RandomData), []byte(gotChallenge)) != 1 {
		return challenge.NewError("validate", "invalid response", 
			fmt.Errorf("%w: wanted response %s but got %s", challenge.ErrFailed, in.Challenge.RandomData, gotChallenge))
	}

	return nil
}
Source: lib/challenge/metarefresh/metarefresh.go:51-64

Registration

Register your challenge implementation in an init() function:
func init() {
	challenge.Register("mycustom", &MyCustomImpl{)
}
The registry is thread-safe and uses sync.RWMutex for concurrent access. Source: lib/challenge/interface.go:15-32

Error Handling

Use the challenge error constructors for consistent error reporting:
var (
	ErrFailed        = errors.New("challenge: user failed challenge")
	ErrMissingField  = errors.New("challenge: missing field")
	ErrInvalidFormat = errors.New("challenge: field has invalid format")
)

func NewError(verb, publicReason string, privateReason error) *Error {
	return &Error{
		Verb:          verb,
		PublicReason:  publicReason,
		PrivateReason: privateReason,
		StatusCode:    http.StatusForbidden,
	}
}
Source: lib/challenge/error.go:9-22

Best Practices

  1. Security-first: Use constant-time comparison for secrets (crypto/subtle.ConstantTimeCompare)
  2. Difficulty scaling: Honor in.Rule.Challenge.Difficulty from the policy configuration
  3. Localization: Use localization.GetLocalizer(r) for internationalized UI
  4. Metrics: Emit Prometheus metrics for observability (see lib/challenge/metrics.go)
  5. Structured logging: Use the provided *slog.Logger for diagnostic output
  6. Templ components: Return templ.Component for HTML rendering consistency

Configuration

Once registered, reference your challenge in policy files:
bots:
  - name: custom-challenge-rule
    action: challenge
    expression:
      - path.startsWith('/protected')
    challenge:
      algorithm: mycustom
      difficulty: 5

Available Challenge Methods

Query registered challenges at runtime:
methods := challenge.Methods()  // Returns []string of all registered challenge types
impl, ok := challenge.Get("fast")  // Get a specific implementation
Source: lib/challenge/interface.go:27-42