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

limes

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2026 License: Apache-2.0 Imports: 45 Imported by: 0

README

limes

ci release go license

Budgeted, delegable, individually revocable capabilities for agentic workloads, as a Go library.

A capability is a short-lived signed token that carries its own authority: which operations, on which resources, for how much money and how many requests, until when, and on whose behalf. An agent can hand a sub-agent a strictly narrower copy without calling back to anyone, every charge counts against the whole delegation chain, and any capability can be revoked mid-flight along with everything delegated from it.

The module ships the primitive and its contracts; you supply the storage. Its resolved dependency graph holds no database driver and no storage SDK, and memstore, an in-memory implementation, is enough to start.

Limes was the fortified frontier of the Roman Empire — a line that marks how far authority reaches (Wiktionary, Wikipedia).

flowchart LR
    op(["Operator"])
    orch(["Orchestrator agent"])
    worker(["Sub-agent"])
    svc["<b>Your service</b><br/>Verify · Caveats.Check · Charge"]
    store[("<b>Your storage</b><br/>Store · UsageStore · KeyResolver")]

    op -- "Issue" --> orch
    orch -- "Delegate / Attenuate<br/>narrower only" --> worker
    orch -- "call + token" --> svc
    worker -- "call + token" --> svc
    svc --> store
    op -. "Revoke" .-> store

    classDef client fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E
    classDef role fill:#DCFCE7,stroke:#16A34A,color:#14532D
    classDef store fill:#FEF3C7,stroke:#D97706,color:#78350F
    class op,orch,worker client
    class svc role
    class store store

Install

go get github.com/oleg-tkachuk/limes@latest

Five minutes

kid, pub, priv, _ := limes.GenerateEd25519Keypair()

records := memstore.New[struct{}]()          // your Store
usage   := memstore.NewUsage[struct{}](records)

signer, _ := limes.NewEd25519Signer(kid, priv)
issuer, _ := limes.NewIssuer(limes.IssuerConfig{
    Signer: signer, Store: records, IssuerName: "my-issuer",
})
verifier, _ := limes.NewStandardVerifier(limes.VerifierConfig{
    Keys:           limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
    Revocations:    records,
    TrustedIssuers: []string{"my-issuer"},
})

cap, token, _ := issuer.Issue(ctx, limes.IssueRequest{
    IssuedBy: limes.Principal{Subject: "operator@example.com"}, // who asked
    Subject: limes.Principal{
        Type: limes.PrincipalAgent, TenantID: tenantID,
        Subject: "research-orchestrator",
        Agent:   &limes.AgentPrincipal{AgentType: "my-agent", RunID: runID},
    },
    Audience: []string{"my-service"},
    TTL:      15 * time.Minute,
    Caveats: limes.Caveats{
        Ops:              []limes.Op{limes.OpGet, limes.OpShare},
        ResourcePrefixes: []string{"corpus/public/"},
        MaxRequests:      500,
        MaxBudgetAmount:  limes.MustParseAmount("25.00"),
        UnitCode:         "USD",
    },
})

// On every call: verify locally and enforce the caveats in one step, then meter.
cap, err := verifier.Authorize(ctx, limes.AuthorizeRequest{
    VerifyRequest: limes.VerifyRequest{Token: token, Audience: "my-service"},
    CheckRequest:  limes.CheckRequest{Op: limes.OpGet, Resource: "corpus/public/report.pdf"},
})
_, err = usage.Charge(ctx, limes.ChargeRequest{
    CapabilityID: cap.ID, TenantID: tenantID, Amount: limes.MustParseAmount("0.35"),
    MaxBudget: cap.Caveats.MaxBudgetAmount, UnitCode: "USD",
}, nil)

token is what the agent carries; cap is the record you keep for audit. go run ./example runs issue, verify, delegate, charge and revoke end to end.

What it does

Verify Local Ed25519 JWT verification (alg: Ed25519, RFC 9864) against a resolved key — static, or an issuer's JWKS fetched and cached by RemoteJWKSResolver. Every rejection is a distinct sentinel error; mapping it to a status code is yours.
Enforce Authorize verifies and checks every stateless caveat in one call: allowed ops, resource prefixes matched at / boundaries, idempotency keys for writes, tainted reads, source networks.
Delegate Issuer.Delegate issues a child that may only be narrower; anything wider is ErrDelegationTooWide.
Attenuate offline The Biscuit v3 form of a capability can be narrowed by its holder with no key and no network; each copy can get its own limits and be revoked on its own.
Bind to a key RFC 9449 DPoP: Verify refuses a key-bound capability without a fresh proof from that key.
Meter Requests and spend count against the capability, every ancestor and the tenant, atomically with your own side effect. Reserve an estimate, settle the actual cost, refund a charge. Amounts are exact integers of billionths of a unit.
Revoke One call stops a capability and everything delegated from it; verifiers learn of it within a cache TTL, or at once on Clear.

You implement Store, UsageStore[TX] and KeyResolver — or use memstore — and check your implementation from its own tests with storetest and metertest. UsageStore is generic in your transaction type, so a charge can commit together with, say, an outbox write; with no transactions, use UsageStore[struct{}].

Guarantees

  • Charges are atomic. A failed side effect, or a rejection by any ceiling, leaves every counter and the ledger untouched, so a retry is safe.
  • Delegation and attenuation never widen. A fuzz test (FuzzNarrowsNeverWidens) checks that whatever Narrows accepts reaches no resource the parent cannot; an offline Biscuit block that asks for more invalidates the token.
  • A bound token needs its key, and its children stay bound.
  • The token format is pinned. Golden fixtures in testdata/ fail CI on any wire-format drift, because tokens outlive deployments.
  • No hidden dependencies. isolation_test.go fails if a database driver or an object-storage SDK enters the resolved dependency graph.
  • Tests need nothing: no database, no network, no container.

Versioning

Releases are vX.Y.Z tags, cut from Conventional Commits on main when CI passes: feat is a minor, fix a patch. The module is pre-1.0, so a breaking change is a minor too — the Go API may change between minor versions; read the release notes before bumping.

The token wire format — claims, typ: limes-cap-v3+jwt, the Biscuit facts — is a separate promise: a token is a credential that outlives the process that issued it, so the format changes only as a deliberate breaking change that moves the version in typ, with the golden fixtures regenerated in the same commit. A verifier refuses a token of another version outright, rather than read its claims under the wrong schema.

Documentation

Document Covers
docs/guide.md each concern in turn: verification, caveats, delegation, DPoP, Biscuit, metering, revocation, storage contracts, key rotation
docs/standards.md the RFCs implemented — JWT, DPoP, JWKS, HTTP challenges and problem details — the choices made within them, and the OAuth standards deliberately left out
docs/diagrams.md the contract boundary, a capability's lifecycle, the verification gates, delegation, Biscuit copies, the charge path, revocation
CONTRIBUTING.md building, testing and how a change lands
SECURITY.md reporting a vulnerability

Layout

Path Holds
/ the limes package: issuer, verifier, caveats, Biscuit, DPoP, metering, contracts
memstore/ the in-memory reference implementation of every storage contract
storetest/, metertest/ conformance suites to run from your own store's tests
example/ the five-minute walkthrough as a runnable program
testdata/ golden tokens, and the Biscuit vocabulary and cases shared with SDKs in other languages
tools/ the developer tools, pinned in modules of their own and run with go tool -modfile=tools/go.mod (yq: tools/yq/go.mod)

License

Apache-2.0 — see LICENSE and NOTICE.

Documentation

Overview

Package limes is an object-capability authorisation primitive designed for agentic workloads.

A Capability is a short-lived, signed token granting its bearer a specific set of operations on a specific set of resources, with explicit budget and lifetime caveats. It is delegable — a parent issues a strictly narrower child to a sub-agent — and individually revocable. Compared with long-lived API keys or role-based access it gives:

  • Per-call attribution: every call carries the capability ID, so audit and cost roll up by it, down to which run of which agent spent what.
  • Budget enforcement at the authorisation boundary: the Meter refuses a charge once a ceiling is exhausted, rather than business logic discovering it three calls deep.
  • Mid-flight revocation: a compromised agent's capability is revoked atomically, optionally cascading to everything it delegated.
  • Sub-capabilities without a round trip: an orchestrator narrows its own authority for each sub-agent it spawns, with no admin API in the path.

Wire format is JWT compact serialization with Ed25519 signatures (EdDSA). Verification is local: the signature is checked against a resolved public key, so consumers scale horizontally without contending on a shared store.

What you implement

Three contracts are required; a consumer that implements them needs nothing else:

  • Store — capability records and revocations. IsRevoked answers for the capability's whole delegation chain. No transactions required; an in-memory map is a valid implementation.
  • UsageStore — request and spend counters and the charges ledger; the union of Meter, TenantBudgets and UsageHousekeeping. Every charge and request also counts against each ancestor. Generic in TX, the consumer's transaction handle; see "No-transaction mode" below.
  • KeyResolver — public verification keys. StaticKeyResolver ships as a working implementation and supports rotation via SetKey / RemoveKey; RemoteJWKSResolver fetches an issuer's JWKS for verifiers that run apart from it.

Three more are needed only with the feature they serve: BiscuitRevocationStore to accept Biscuit tokens, CopyUsageReader to show a Biscuit copy's own counters, and ReplayCache to share DPoP replay state across replicas (MemoryReplayCache ships for one process).

A Capability crosses the API by value, in and out: no caller can alter one another holds, and one that is not there is an error, never nil.

Every amount is Nanos, an int64 count of billionths of the unit (UnitCode, an ISO 4217 code; XXX for a budget that is not money), so spend sums, compares and refunds exactly, with no rounding left to the consumer.

Every read names what it reads — Store.Get a record, Meter.GetUsage the counters, TenantBudgets.GetTenantBudget a ceiling — so one type may implement Store and UsageStore together, or each apart as memstore does.

No-transaction mode

Meter.Charge and Meter.Settle take an onCharged callback that runs inside the charge, receiving the consumer's transaction handle, so a side effect such as an outbox write commits atomically with the spend. The module never constructs, inspects or constrains that handle — it only threads it back. That is what keeps this package free of any database dependency.

A consumer with no transactional storage instantiates UsageStore[struct{}] and always passes nil for onCharged. Everything else behaves identically; only the atomic-side-effect guarantee is forgone, and that limitation is store-dependent rather than a degradation of the primitive.

Guarantees

Rejections are individually matchable sentinels, so a consumer can map each to its own transport status; the module does not choose status codes. A charge rejected by any ceiling — the capability's, an ancestor's, the tenant's — leaves every counter and the ledger unmutated, so retrying after a rejection is safe. Delegation can only narrow: any widening returns ErrDelegationTooWide, and a differing unit code returns ErrUnitCodeMismatch rather than converting.

Enforcing caveats

Caveats.Check and Caveats.CheckSource are the single definition of what the caveats mean at use time; delegation narrowing (Narrows) uses the same resource matcher (MatchResource), so the two cannot disagree.

See README.md for a five-minute walkthrough and example/ for it as a runnable program.

Example (Charge)
package main

import (
	"context"
	"crypto/ed25519"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

	"github.com/oleg-tkachuk/limes"
	"github.com/oleg-tkachuk/limes/memstore"
)

// The issuer's name, and the audience every example's tokens are minted for.
const (
	exampleIssuerName = "example-issuer"
	exampleAudience   = "my-service"
)

// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
	issuer   *limes.Issuer
	verifier *limes.StandardVerifier
	records  *memstore.Store[struct{}]
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	kid, pub, priv, err := limes.GenerateEd25519Keypair()
	if err != nil {
		log.Fatal(err)
	}
	records := memstore.New[struct{}]()
	signer, err := limes.NewEd25519Signer(kid, priv)
	if err != nil {
		log.Fatal(err)
	}
	issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewStandardVerifier(limes.VerifierConfig{
		Keys:               limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               &limes.DPoPVerifier{Replay: limes.NewMemoryReplayCache(0)},
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: memstore.NewUsage[struct{}](records), tenantID: uuid.New(),
	}
}

// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
	c, token, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
		},
		Audience: []string{exampleAudience},
		TTL:      15 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet, limes.OpShare},
			ResourcePrefixes: []string{"corpus/public/"},
			MaxRequests:      500,
			MaxBudgetAmount:  limes.MustParseAmount("25.00"),
			UnitCode:         "USD",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	c, _ := s.issue()

	charge := func(amount string) (limes.ChargeReceipt, error) {
		return s.usage.Charge(ctx, limes.ChargeRequest{
			CapabilityID: c.ID, TenantID: s.tenantID, Amount: limes.MustParseAmount(amount),
			MaxBudget: c.Caveats.MaxBudgetAmount, UnitCode: "USD", Op: "search",
		}, nil)
	}

	receipt, err := charge("0.35")
	fmt.Println(err, receipt.Spent)

	// Over the ceiling: refused, and no counter moves.
	_, err = charge("999")
	fmt.Println(errors.Is(err, limes.ErrBudgetExceeded))

	refund, err := s.usage.Refund(ctx, limes.RefundRequest{ChargeID: receipt.ChargeID})
	fmt.Println(err, refund.Refunded)
}
Output:
<nil> 0.35
true
<nil> 0.35
Example (Revoke)
package main

import (
	"context"
	"crypto/ed25519"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

	"github.com/oleg-tkachuk/limes"
	"github.com/oleg-tkachuk/limes/memstore"
)

// The issuer's name, and the audience every example's tokens are minted for.
const (
	exampleIssuerName = "example-issuer"
	exampleAudience   = "my-service"
)

// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
	issuer   *limes.Issuer
	verifier *limes.StandardVerifier
	records  *memstore.Store[struct{}]
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	kid, pub, priv, err := limes.GenerateEd25519Keypair()
	if err != nil {
		log.Fatal(err)
	}
	records := memstore.New[struct{}]()
	signer, err := limes.NewEd25519Signer(kid, priv)
	if err != nil {
		log.Fatal(err)
	}
	issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewStandardVerifier(limes.VerifierConfig{
		Keys:               limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               &limes.DPoPVerifier{Replay: limes.NewMemoryReplayCache(0)},
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: memstore.NewUsage[struct{}](records), tenantID: uuid.New(),
	}
}

// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
	c, token, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
		},
		Audience: []string{exampleAudience},
		TTL:      15 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet, limes.OpShare},
			ResourcePrefixes: []string{"corpus/public/"},
			MaxRequests:      500,
			MaxBudgetAmount:  limes.MustParseAmount("25.00"),
			UnitCode:         "USD",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	c, token := s.issue()

	err := s.records.Revoke(ctx, limes.RevokeRequest{
		ID: c.ID, Reason: "agent looping", Actor: "operator@example.com", CascadeChildren: true,
	})
	fmt.Println(err)

	_, err = s.verifier.Verify(ctx, limes.VerifyRequest{Token: token, Audience: exampleAudience})
	fmt.Println(errors.Is(err, limes.ErrRevoked))
}
Output:
<nil>
true

Index

Examples

Constants

View Source
const (
	// HMACNonceKeyBytes is the least key length NewHMACNonces accepts: the
	// output size of the HMAC's hash.
	HMACNonceKeyBytes = sha256.Size
	// DefaultDPoPNonceLifetime is how long a nonce is accepted when
	// NewHMACNonces is given no lifetime.
	DefaultDPoPNonceLifetime = 5 * time.Minute
)

HMACNonce sizes and defaults.

View Source
const (
	DefaultListLimit = 50
	MaxListLimit     = 500
)

Page sizes of ListByPrincipal: Limit 0 or below means DefaultListLimit, and anything above MaxListLimit is clamped to it.

View Source
const AbstractUnitCode = "XXX"

AbstractUnitCode is the unit of a budget that is not money — requests, credits, anything metered: ISO 4217's XXX, "no currency involved".

View Source
const AuthorizationHeader = "Authorization"

AuthorizationHeader is the request header a token travels in.

View Source
const DPoPHeader = "DPoP"

DPoPHeader is the HTTP header a proof travels in.

View Source
const DPoPNonceHeader = "DPoP-Nonce"

DPoPNonceHeader is the HTTP response header a nonce travels in (RFC 9449 §8).

View Source
const DPoPTokenType = "dpop+jwt"

DPoPTokenType is the JWT "typ" of a proof.

View Source
const DefaultDPoPWindow = time.Minute

DefaultDPoPWindow is how far a proof's iat may be from the verifier's clock.

View Source
const DefaultMaxTokenBytes = 16 << 10

DefaultMaxTokenBytes bounds the token a verifier will look at. A capability with a few dozen resource entries is a couple of kilobytes; anything far larger is refused before any decoding is spent on it.

View Source
const DefaultReservationTTL = 5 * time.Minute

DefaultReservationTTL is how long a reservation lasts when the request does not say.

View Source
const DefaultRevocationCacheEntries = 100_000

DefaultRevocationCacheEntries bounds the cache. Past it, the oldest entry makes room for the new one: a flood of distinct ids costs extra lookups, never unbounded memory, and never the answers cached for everyone else.

View Source
const DefaultRevocationCacheTTL = 2 * time.Second

DefaultRevocationCacheTTL bounds how long a cached answer is served. A revocation therefore takes effect within this window on every verifier that did not perform it.

View Source
const DefaultUnitCode = "USD"

DefaultUnitCode is the fallback when a request / row omits the unit. Keeps existing payloads / DB rows working without an explicit migration on the domain side.

View Source
const MaxExternalRefBytes = 200

MaxExternalRefBytes bounds ChargeRequest.ExternalRef: long enough for any UUID, ULID or provider call id, short enough to index.

View Source
const MaxIJSONInteger = 1<<53 - 1

MaxIJSONInteger is the largest integer a token carries: 2^53 − 1, the largest an IEEE 754 double holds exactly. RFC 7493 (I-JSON) §2.2 keeps numbers within it so that every JSON reader — JavaScript's among them — reads the same value; larger amounts travel as decimal strings (Nanos).

View Source
const MediaTypeProblem = "application/problem+json"

MediaTypeProblem is RFC 9457 §3's media type for problem details.

View Source
const NanosPerUnit = 1_000_000_000

NanosPerUnit is how many nanos make one unit.

View Source
const TokenType = "limes-cap-v3+jwt" //nolint:gosec // G101: a JWT type label, not a credential

TokenType is the JWT "typ" header every capability token carries. The verifier requires it, so a different kind of EdDSA JWT signed by the same key — an ID token, another product's token — cannot be presented as a capability. It names the format's version: a token of another version is refused here, before a claim of it is read under the wrong schema.

View Source
const WWWAuthenticateHeader = "WWW-Authenticate"

WWWAuthenticateHeader is the response header a challenge travels in.

Variables

View Source
var (
	ErrOpNotAllowed           error = &caveatError{"limes: operation not allowed"}
	ErrResourceNotAllowed     error = &caveatError{"limes: resource not allowed"}
	ErrSourceIPNotAllowed     error = &caveatError{"limes: source IP not allowed"}
	ErrIdempotencyKeyRequired error = &caveatError{"limes: idempotency key required"}
	ErrTaintedReadNotAllowed  error = &caveatError{"limes: tainted read not allowed"}
)

Request-time caveat rejections. Each also matches ErrCaveatViolation.

View Source
var (
	// ErrDPoPRequired — the capability is key-bound and no proof came with it.
	ErrDPoPRequired error = &popError{"limes: DPoP proof required"}
	// ErrDPoPInvalid — a proof came, and it does not prove possession for
	// this token, method, URL and moment.
	ErrDPoPInvalid error = &popError{"limes: invalid DPoP proof"}
	// ErrDPoPReplayed — a proof's jti was already used inside the window.
	ErrDPoPReplayed error = &popError{"limes: DPoP proof replayed"}
)

Proof-of-possession rejections. Each also matches ErrInvalidSignature, so a consumer that maps only that sentinel still refuses them.

View Source
var (
	ErrInvalidSignature  = errors.New("limes: invalid signature")
	ErrExpired           = errors.New("limes: expired")
	ErrNotYetValid       = errors.New("limes: not yet valid")
	ErrRevoked           = errors.New("limes: revoked")
	ErrCaveatViolation   = errors.New("limes: caveat violation")
	ErrAudienceMismatch  = errors.New("limes: audience mismatch")
	ErrBudgetExceeded    = errors.New("limes: budget exceeded")
	ErrDelegationTooWide = errors.New("limes: child wider than parent")
	// ErrUnitCodeMismatch — two units that would have to be one: a
	// delegated child declaring a unit_code other than its parent's, or an
	// amount in a unit other than the one a counter (a capability's, an
	// ancestor's, a tenant's) already counts in. Nothing converts between
	// currencies, so cross-currency delegation is refused at issuance and a
	// cross-currency charge at the Meter.
	ErrUnitCodeMismatch = errors.New("limes: unit_code mismatch")
	// ErrInvalidRequest is an Issue or Delegate request that is malformed in
	// itself: a missing tenant, an empty audience entry, a negative TTL, a
	// thumbprint that is not one, a not-before past the expiry, caveats that
	// do not validate. Nothing was minted. Every such error matches it, so a
	// caller maps the whole family to its "bad request" without listing it.
	ErrInvalidRequest = errors.New("limes: invalid request")
	// ErrUnknownTenant is an issuance for a tenant that does not exist —
	// typically one whose provisioning has not finished. The Store returns it.
	ErrUnknownTenant = errors.New("limes: unknown tenant")
	// ErrTenantDeleted is an issuance for a tenant in the trash: it exists,
	// so its rows may be restored, but nothing new is minted for it. The
	// Store returns it.
	ErrTenantDeleted = errors.New("limes: tenant deleted")
)

Errors exposed to callers. Mapped to transport errors by the consumer — not here. Verifier returns these so callers can switch.

View Source
var (
	// ErrInvalidAmount — a charge or refund amount that is negative, NaN
	// or infinite. Refunds are explicit; a negative charge is not one.
	ErrInvalidAmount = errors.New("limes: invalid amount")

	// ErrChargeNotFound — Refund named a charge with no ledger row.
	ErrChargeNotFound = errors.New("limes: charge not found")

	// ErrRefundExceedsCharge — Refund would return more than the charge
	// took, counting earlier refunds of the same charge.
	ErrRefundExceedsCharge = errors.New("limes: refund exceeds charge")

	// ErrReservationNotFound — Settle named a reservation that does not
	// exist, was released, or has expired. One already settled is not an
	// error: Settle returns its charge again (ChargeReceipt.Replayed).
	ErrReservationNotFound = errors.New("limes: reservation not found")

	// ErrRequestLimitExceeded — capability used MaxRequests times,
	// next attempt was blocked.
	ErrRequestLimitExceeded = errors.New("limes: request limit exceeded")

	// ErrUsageNotFound — no usage row exists for this capability.
	// Get-only; bump/charge always inserts on first hit.
	ErrUsageNotFound = errors.New("limes: usage row not found")

	// ErrTenantBudgetExceeded — the tenant aggregate cap has been
	// hit. Distinct from ErrBudgetExceeded so admin tooling can
	// render "this tenant is out of budget" vs. "this capability is
	// out of budget". Operators handle differently — tenant lift
	// is an admin RPC; capability lift requires re-issuing the
	// caveat.
	ErrTenantBudgetExceeded = errors.New("limes: tenant aggregate budget exceeded")

	// ErrTenantBudgetNotFound — Get-only; the tenant has no cap
	// configured AND has never been charged. Distinct from "cap = 0"
	// (which means "configured but unlimited").
	ErrTenantBudgetNotFound = errors.New("limes: tenant budget row not found")

	// ErrTenantBudgetVersionMismatch — SetTenantBudget's OCC guard refused the
	// write: the stored resource_version is not the one the caller read, so
	// someone else changed the cap in between. The caller re-reads and decides,
	// rather than silently overwriting a change it never saw.
	ErrTenantBudgetVersionMismatch = errors.New("limes: tenant budget resource_version mismatch")
)

Usage-related sentinels. ErrBudgetExceeded already lives in types.go (declared before the runtime enforcement landed); reused here.

View Source
var ErrAlreadyExists = errors.New("limes: already exists")

ErrAlreadyExists is Store.Insert given an id already on record.

View Source
var ErrAuthSchemeMismatch error = &popError{"limes: token presented under the wrong scheme"}

ErrAuthSchemeMismatch — the scheme the token came under is not the one its binding calls for: a key-bound token as Bearer, which RFC 9449 §7.2 treats as a downgrade, or an unbound one as DPoP. Matches ErrInvalidSignature.

View Source
var ErrBiscuitAttenuation error = &popError{"limes: invalid Biscuit attenuation"}

ErrBiscuitAttenuation — an attenuation block the verifier cannot honour: outside the vocabulary, malformed, or widening its parent. Matches ErrInvalidSignature, so consumers that map only that sentinel refuse it.

View Source
var ErrCopyCountersNotMetered = fmt.Errorf("%w: copy limits need a verifier with MeterCopies", ErrBiscuitAttenuation)

ErrCopyCountersNotMetered — a Biscuit sets a copy's own request or budget limit, and the verifier was not told that its Meter counts copies. The limit would go unenforced, so the token is refused instead.

View Source
var ErrCursorRepeated = errors.New("limes: a page returned the cursor it was fetched with")

ErrCursorRepeated is a page that hands back the cursor it was fetched with. Following it would fetch the same page forever, so the walk stops there.

View Source
var ErrDPoPNonceRequired error = &popError{"limes: DPoP nonce required"}

ErrDPoPNonceRequired — the verifier requires a server-provided nonce and the proof carried none, or one it did not issue or that has expired. The response carries a fresh one (StandardVerifier.DPoPNonce). Matches ErrInvalidSignature, as every proof-of-possession refusal does.

View Source
var ErrEffectConflict error = &requestError{"limes: declared effect conflicts with the operation"}

ErrEffectConflict is a declared effect that contradicts a built-in operation's own ("put" declared a read), or one this package does not define. A declaration may name a built-in operation's effect, never change it: a write declared a read would shed IdempotencyKeyRequired. It is a programming error in the consumer, so it matches ErrInvalidRequest.

View Source
var ErrExternalRefConflict error = &requestError{"limes: external ref already names a different charge"}

ErrExternalRefConflict — Charge named an ExternalRef its capability already charged under, for a different amount, unit or tenant. One reference names one cost; replaying the first receipt would report the second as booked. Matches ErrInvalidRequest.

View Source
var ErrInvalidCaveats error = &requestError{"limes: invalid caveats"}

ErrInvalidCaveats is returned when a caveat bag is malformed: a negative or non-finite budget, an unknown unit, an empty resource entry, an unparsable CIDR, an operation name that is not well formed. It is a request error, not an authorisation decision — the capability was never minted — so it also matches ErrInvalidRequest.

View Source
var ErrJWKSUnavailable = errors.New("limes: JWKS unavailable")

ErrJWKSUnavailable is returned by RemoteJWKSResolver when it holds no key set recent enough to trust: the endpoint has been failing for longer than MaxStale, or never answered. The verifier turns it into ErrInvalidSignature — an issuer we cannot reach verifies nothing.

View Source
var ErrMalformedAuthorization = errors.New("limes: malformed Authorization header")

ErrMalformedAuthorization — an Authorization header that is not one scheme this module reads followed by one token68 credential. It is a malformed request (RFC 6750 §3.1 invalid_request), not a bad token.

View Source
var ErrNotFound = errors.New("limes: not found")

ErrNotFound is the typed not-found return from Store.Get. It lives in the core package rather than in a store implementation so that any consumer — in-memory, relational, or otherwise — can satisfy the Store contract using only what this package publishes.

Verifiers map it to an invalid-token response: a syntactically valid token whose record is absent was forged or purged, which is an authentication failure, not a missing entity.

View Source
var ErrUnknownKID = errors.New("limes: unknown kid")

ErrUnknownKID is the typed not-found return from KeyResolver. The verifier converts it to ErrInvalidSignature for caller-facing errors; keeping the typed return lets ops dashboards distinguish "we don't know this kid" from "signature mismatch".

Functions

func AccessTokenHash

func AccessTokenHash(token string) string

AccessTokenHash is the proof's `ath`: base64url(SHA-256(token)).

func AllByPrincipal added in v0.2.0

AllByPrincipal walks ListByPrincipal page by page, from req.Cursor, and yields every capability in its order. req.Limit sets the page size, not how many are yielded.

An error is yielded once, with a zero Capability, and ends the walk; breaking out of the loop fetches no further page. The cursor is the contract for paging across requests — an API handing a page token to its client — and this is for a caller that wants the whole list in one go.

Example
package main

import (
	"context"
	"crypto/ed25519"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

	"github.com/oleg-tkachuk/limes"
	"github.com/oleg-tkachuk/limes/memstore"
)

// The issuer's name, and the audience every example's tokens are minted for.
const (
	exampleIssuerName = "example-issuer"
	exampleAudience   = "my-service"
)

// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
	issuer   *limes.Issuer
	verifier *limes.StandardVerifier
	records  *memstore.Store[struct{}]
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	kid, pub, priv, err := limes.GenerateEd25519Keypair()
	if err != nil {
		log.Fatal(err)
	}
	records := memstore.New[struct{}]()
	signer, err := limes.NewEd25519Signer(kid, priv)
	if err != nil {
		log.Fatal(err)
	}
	issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewStandardVerifier(limes.VerifierConfig{
		Keys:               limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               &limes.DPoPVerifier{Replay: limes.NewMemoryReplayCache(0)},
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: memstore.NewUsage[struct{}](records), tenantID: uuid.New(),
	}
}

// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
	c, token, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
		},
		Audience: []string{exampleAudience},
		TTL:      15 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet, limes.OpShare},
			ResourcePrefixes: []string{"corpus/public/"},
			MaxRequests:      500,
			MaxBudgetAmount:  limes.MustParseAmount("25.00"),
			UnitCode:         "USD",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	for range 3 {
		s.issue()
	}

	// One page per capability, to show the walk crossing pages.
	const pageSize = 1
	n := 0
	for _, err := range limes.AllByPrincipal(ctx, s.records, limes.ListByPrincipalRequest{
		TenantID: s.tenantID, PrincipalType: limes.PrincipalAgent, Subject: "research-orchestrator",
		Limit: pageSize,
	}) {
		if err != nil {
			log.Fatal(err)
		}
		n++
	}
	fmt.Println(n)
}
Output:
3

func AllTenantBudgets added in v0.2.0

AllTenantBudgets walks ListTenantBudgets page by page, from req.Cursor, and yields every row in its order, as AllByPrincipal does. Spend moves between pages, so a tenant may be yielded twice or not at all; see TenantBudgets.ListTenantBudgets.

func Attenuate

func Attenuate(token string, a Attenuation) (string, error)

Attenuate appends a block that narrows token. It needs no key and no network; the result is a new token, and the input still works as before.

Example
package main

import (
	"context"
	"crypto/ed25519"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

	"github.com/oleg-tkachuk/limes"
	"github.com/oleg-tkachuk/limes/memstore"
)

// The issuer's name, and the audience every example's tokens are minted for.
const (
	exampleIssuerName = "example-issuer"
	exampleAudience   = "my-service"
)

// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
	issuer   *limes.Issuer
	verifier *limes.StandardVerifier
	records  *memstore.Store[struct{}]
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	kid, pub, priv, err := limes.GenerateEd25519Keypair()
	if err != nil {
		log.Fatal(err)
	}
	records := memstore.New[struct{}]()
	signer, err := limes.NewEd25519Signer(kid, priv)
	if err != nil {
		log.Fatal(err)
	}
	issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewStandardVerifier(limes.VerifierConfig{
		Keys:               limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               &limes.DPoPVerifier{Replay: limes.NewMemoryReplayCache(0)},
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: memstore.NewUsage[struct{}](records), tenantID: uuid.New(),
	}
}

// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
	c, token, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
		},
		Audience: []string{exampleAudience},
		TTL:      15 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet, limes.OpShare},
			ResourcePrefixes: []string{"corpus/public/"},
			MaxRequests:      500,
			MaxBudgetAmount:  limes.MustParseAmount("25.00"),
			UnitCode:         "USD",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	c, _ := s.issue()

	full, err := s.issuer.Biscuit(context.Background(), c)
	if err != nil {
		log.Fatal(err)
	}
	// The holder narrows it offline: no key, no network.
	narrow, err := limes.Attenuate(full, limes.Attenuation{
		Ops:              []limes.Op{limes.OpGet},
		ResourcePrefixes: []string{"corpus/public/2026/"},
	})
	if err != nil {
		log.Fatal(err)
	}

	got, err := s.verifier.Verify(ctx, limes.VerifyRequest{Token: narrow, Audience: exampleAudience})
	fmt.Println(err, got.ID == c.ID, got.Caveats.Ops, got.Caveats.ResourcePrefixes)
}
Output:
<nil> true [get] [corpus/public/2026/]

func DPoPSigningAlgorithms added in v0.3.0

func DPoPSigningAlgorithms() []string

DPoPSigningAlgorithms are the JWS algorithms a DPoP proof may be signed with, as a challenge advertises them (RFC 9449 §7.1): the fully-specified names (RFC 9864). The deprecated "EdDSA" is still read, never advertised.

func DeriveKeyID added in v0.3.0

func DeriveKeyID(pub ed25519.PublicKey) string

DeriveKeyID is the kid of a key published with none named: its RFC 7638 thumbprint. The same key always gets the same kid, wherever it is derived, and two keys never share one.

func GenerateEd25519Keypair

func GenerateEd25519Keypair() (kid string, pub ed25519.PublicKey, priv ed25519.PrivateKey, err error)

GenerateEd25519Keypair is a convenience for boot wiring: returns a new Ed25519 keypair plus the kid to publish it under, DeriveKeyID's.

func HasDotSegment added in v0.3.0

func HasDotSegment(s string) bool

HasDotSegment reports whether s has a "/"-separated segment that is "." or "..", counting "%2e" (any case) as a dot — the segments RFC 3986 resolves away, which make a resource's text name somewhere other than it reads. Caveats.Validate refuses a resource with one, and no request naming one matches a restricted capability.

func IsBiscuit

func IsBiscuit(token string) bool

IsBiscuit reports whether token is in the Biscuit form rather than a JWT: a JWT has exactly two dots, base64url has none.

func IsUnitCode added in v0.3.0

func IsUnitCode(u string) bool

IsUnitCode reports whether u is an ISO 4217 currency code, in its canonical upper-case spelling: "USD", "JPY", "XXX". Empty is NOT one — callers that want the empty-means-default semantics resolve through NormaliseUnitCode first.

func JWKSHandler added in v0.3.0

func JWKSHandler(keys func() map[string]ed25519.PublicKey, maxAge time.Duration) http.Handler

JWKSHandler serves keys() as a JWK Set document (RFC 7517), the way the RemoteJWKSResolver on the other end reads it: under its registered media type (§8.5), with a strong ETag of its bytes that answers If-None-Match with 304 (RFC 9110 §13.1.2), and fresh for maxAge (RFC 9111 §5.2.2.1). Keep maxAge below the time between publishing a new key and signing with it, and between withdrawing an old key and the end of its tokens' TTL.

func KeyThumbprint

func KeyThumbprint(pub crypto.PublicKey) (string, error)

KeyThumbprint is PublicJWK followed by Thumbprint: what to put in an IssueRequest's ConfirmationJKT to bind a capability to the holder of pub.

func LoadPrivateKey

func LoadPrivateKey(path string) (ed25519.PrivateKey, error)

LoadPrivateKey reads an Ed25519 private key from a PEM-encoded PKCS#8 file at the supplied path. Production wires this against a Secret-mounted RO volume; dev calls GenerateEd25519Keypair instead and skips this path entirely.

We accept only PKCS#8 (not legacy PEM "PRIVATE KEY" with raw bytes) because that's the OpenSSL-default output and what kubectl-managed secrets typically contain. Wrong format is loud — better than silently working with one form and breaking on the other.

func MarshalJWKS

func MarshalJWKS(keys map[string]ed25519.PublicKey) ([]byte, error)

MarshalJWKS produces a JWKS document for the supplied kid → public key map, with entries sorted by kid so the same key set always serialises to the same bytes (cacheable, diffable, ETag-able).

func MatchResource

func MatchResource(prefix, uri string) bool

MatchResource reports whether uri falls under prefix, matched at a path- segment boundary: "corpus/public" covers "corpus/public" and "corpus/public/x", but not "corpus/public-secret". A prefix that already ends in "/" matches anything beneath it. An empty prefix matches nothing — a caveat that means "everything" is written by leaving the list empty, not by an empty entry, so a stray "" can never silently widen a capability.

A uri with a dot segment — "." or "..", percent-encoded or not — matches nothing: what it names depends on how the consumer resolves it, so the match it seems to make by its text proves nothing about where it leads.

This is the single definition of a resource match. Delegation narrowing and request-time enforcement both go through it, so the two can never disagree about what a prefix reaches.

Example
package main

import (
	"fmt"

	"github.com/oleg-tkachuk/limes"
)

func main() {
	fmt.Println(limes.MatchResource("corpus/public", "corpus/public/x"))
	fmt.Println(limes.MatchResource("corpus/public", "corpus/public-secret"))
}
Output:
true
false

func Narrows

func Narrows(parent, child Capability) error

Narrows reports whether `child` is a narrowing of `parent`: every operation child allows is allowed by parent; every resource child can touch is reachable by parent; the budget / request count are no larger, and the validity window starts no earlier and ends no later; the audience is a subset; tainted-read is no broader. Issuer.Delegate runs this check before signing the child token, so a misuse fails at issue time, not later.

The function is exposed at package level so external tooling can dry-run a delegation without going through the issuer.

func NewDPoPProof

func NewDPoPProof(key crypto.Signer, req DPoPProofRequest) (string, error)

NewDPoPProof signs a proof for one request. key is any crypto.Signer whose public key is Ed25519 or ECDSA P-256.

Example
package main

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

	"github.com/oleg-tkachuk/limes"
	"github.com/oleg-tkachuk/limes/memstore"
)

// The issuer's name, and the audience every example's tokens are minted for.
const (
	exampleIssuerName = "example-issuer"
	exampleAudience   = "my-service"
)

// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
	issuer   *limes.Issuer
	verifier *limes.StandardVerifier
	records  *memstore.Store[struct{}]
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	kid, pub, priv, err := limes.GenerateEd25519Keypair()
	if err != nil {
		log.Fatal(err)
	}
	records := memstore.New[struct{}]()
	signer, err := limes.NewEd25519Signer(kid, priv)
	if err != nil {
		log.Fatal(err)
	}
	issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewStandardVerifier(limes.VerifierConfig{
		Keys:               limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               &limes.DPoPVerifier{Replay: limes.NewMemoryReplayCache(0)},
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: memstore.NewUsage[struct{}](records), tenantID: uuid.New(),
	}
}

// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
	c, token, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
		},
		Audience: []string{exampleAudience},
		TTL:      15 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet, limes.OpShare},
			ResourcePrefixes: []string{"corpus/public/"},
			MaxRequests:      500,
			MaxBudgetAmount:  limes.MustParseAmount("25.00"),
			UnitCode:         "USD",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	const method, url = "POST", "https://data.example.com/objects/get"

	// The capability is bound to a key the agent keeps.
	_, agentKey, _ := ed25519.GenerateKey(rand.Reader)
	jkt, _ := limes.KeyThumbprint(agentKey.Public())
	_, token, err := s.issuer.Issue(ctx, limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject:  limes.Principal{Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "worker"},
		Audience: []string{exampleAudience}, TTL: 15 * time.Minute,
		Caveats:         limes.Caveats{Ops: []limes.Op{limes.OpGet}},
		ConfirmationJKT: jkt,
	})
	if err != nil {
		log.Fatal(err)
	}

	// Without a proof the token is refused: a copy of it is useless.
	_, err = s.verifier.Verify(ctx, limes.VerifyRequest{Token: token, Audience: exampleAudience})
	fmt.Println(errors.Is(err, limes.ErrDPoPRequired))

	// The client signs a fresh proof per request.
	proof, _ := limes.NewDPoPProof(agentKey, limes.DPoPProofRequest{Method: method, URL: url, Token: token, Now: time.Now()})
	req := limes.VerifyRequest{
		Token: token, Audience: exampleAudience,
		DPoP: limes.DPoPProof{Proof: proof, Method: method, URL: url},
	}
	_, err = s.verifier.Verify(ctx, req)
	fmt.Println(err)

	// The same proof a second time is a replay.
	_, err = s.verifier.Verify(ctx, req)
	fmt.Println(errors.Is(err, limes.ErrDPoPReplayed))
}
Output:
true
<nil>
true

func NormaliseUnitCode

func NormaliseUnitCode(u string) (string, error)

NormaliseUnitCode applies the empty-means-default policy and validates the result. Returns the canonical unit string and an error when the supplied non-empty value is not an ISO 4217 code.

func PageLimit

func PageLimit(requested int32) int32

PageLimit is the page size a store serves for a requested limit.

func ParseJWKS

func ParseJWKS(raw []byte) (map[string]ed25519.PublicKey, error)

ParseJWKS decodes a JWKS document into a kid → public key map. Entries that are not Ed25519 (a future kty), that lack a kid, or whose key does not decode to an Ed25519 public key are skipped rather than failing the document: one malformed or foreign entry must not take every other key — and so every token signed with them — out of service during a rotation. A document that is not JSON at all is an error.

func ValidateAmount

func ValidateAmount(amount Nanos) error

ValidateAmount reports whether a charge or refund amount is usable: not negative and at most MaxNanos. Implementations call it before touching state.

func ValidateExternalRef

func ValidateExternalRef(ref string) error

ValidateExternalRef reports whether ref is usable as ChargeRequest.ExternalRef: empty, or printable text within MaxExternalRefBytes.

func VerifySignature

func VerifySignature(token string, pub ed25519.PublicKey) error

VerifySignature checks the Ed25519 signature on a compact token against the supplied public key. Time / audience / revocation gates are NOT applied here — Verify in the production verifier wraps this call with the full check chain.

func WriteErrorResponse added in v0.3.0

func WriteErrorResponse(w http.ResponseWriter, resp ErrorResponse)

WriteErrorResponse writes resp: its status, challenges, nonce and problem.

Types

type AgentPrincipal

type AgentPrincipal struct {
	// AgentType is a human-meaningful label ("research-orchestrator",
	// "code-reviewer-bot"). Used for grouping, dashboards, anomaly
	// baselines.
	AgentType string `json:"agent_type,omitempty"`
	// AgentVersion pins the build / git-sha of the agent runtime.
	AgentVersion string `json:"agent_version,omitempty"`
	// RunID is a UUID minted by the agent host at session start.
	RunID uuid.UUID `json:"run_id,omitzero"`
	// ParentAgentID is set when this agent was spawned by another via
	// `OpShare`. Empty for top-level agents.
	ParentAgentID uuid.UUID `json:"parent_agent_id,omitzero"`
	// Model identifies the LLM behind the agent ("example-model-1",
	// "gpt-5-pro"). Used for cost attribution.
	Model string `json:"model,omitempty"`
	// MCPClient is the agent host's user-agent ("example-host@1.4.0",
	// "cursor@0.44"). Useful for compatibility tracking.
	MCPClient string `json:"mcp_client,omitempty"`
}

AgentPrincipal carries the fields specific to AI-agent principals. Threaded into audit + lineage so per-tool-call replay reconstructs the call graph without joining across half a dozen tables.

type Attenuation

type Attenuation struct {
	// Ops the token keeps.
	Ops []Op
	// ResourcePrefixes and ResourceURIs the token keeps. Setting either
	// replaces both.
	ResourcePrefixes []string
	ResourceURIs     []string
	// Planes (audiences) the token keeps.
	Planes []string
	// ExpiresAt, when earlier than the token's, becomes its expiry.
	ExpiresAt time.Time
	// ConfirmationJKT binds the token to a key (KeyThumbprint). Only an
	// unbound token can be bound offline: a bound one stays bound to its key,
	// because rebinding needs no key and anyone could do it.
	ConfirmationJKT string
	// MaxRequests and MaxBudget give this copy limits of its own, counted
	// apart from its siblings' and within the capability's: requests made
	// with it, and its spend in the capability's unit. 0 sets none. Each
	// must be within every limit already in force.
	MaxRequests int64
	MaxBudget   Nanos
}

Attenuation is one offline narrowing. Empty fields leave that dimension as it is; set ones replace it and must be within what the token already allows, or the verifier refuses the whole token.

type AuthScheme added in v0.3.0

type AuthScheme string

AuthScheme is the authentication scheme a token was presented under.

const (
	// AuthSchemeUnspecified is a token that did not come from an
	// Authorization header; VerifyRequest checks no scheme for it.
	AuthSchemeUnspecified AuthScheme = ""
	// AuthSchemeBearer presents a bearer token (RFC 6750).
	AuthSchemeBearer AuthScheme = "Bearer"
	// AuthSchemeDPoP presents a key-bound token (RFC 9449 §7.1).
	AuthSchemeDPoP AuthScheme = "DPoP"
)

func ParseAuthorization added in v0.3.0

func ParseAuthorization(header string) (AuthScheme, string, error)

ParseAuthorization reads an Authorization header value: the scheme, matched without regard to case (RFC 9110 §11.1), and its token68 credential.

type AuthorizeRequest added in v0.3.0

type AuthorizeRequest struct {
	VerifyRequest
	CheckRequest
	// ClientAddr is the client address the consumer observed. A capability
	// constrained to source networks refuses the zero address.
	ClientAddr netip.Addr
}

AuthorizeRequest is a VerifyRequest and the operation it is presented for.

type BiscuitCopy

type BiscuitCopy struct {
	// CapabilityID is the capability the Biscuit seals.
	CapabilityID uuid.UUID
	// RevocationID is the id of the copy's last block: listing it revokes
	// this copy and every copy attenuated from it.
	RevocationID []byte
	// Limits are the request and budget limits in force on the copy — its
	// own and those of the copies it was attenuated from — innermost first;
	// CopyUsageReader reads their counters.
	Limits []CopyCeiling
}

BiscuitCopy identifies one copy of a capability's Biscuit.

type BiscuitRevocation

type BiscuitRevocation struct {
	CapabilityID uuid.UUID
	RevocationID []byte
	RevokedAt    time.Time
	Reason       string
	Actor        string
}

BiscuitRevocation is one revoked Biscuit copy, as RevokeBiscuit wrote it.

type BiscuitRevocationLookup

type BiscuitRevocationLookup interface {
	IsBiscuitRevoked(ctx context.Context, revocationIDs [][]byte) (bool, error)
}

BiscuitRevocationLookup answers whether any of a Biscuit's revocation ids is revoked. The ids it is asked about come from a Biscuit whose signature chain has already verified.

type BiscuitRevocationStore

type BiscuitRevocationStore interface {
	BiscuitRevocationLookup

	// RevokeBiscuit lists one revocation id of a capability's Biscuit.
	// Idempotent. Returns ErrNotFound when the capability is not on record
	// — or not visible to the caller — so a caller cannot revoke a copy of a
	// capability it cannot see, and an ErrInvalidRequest for an empty
	// revocation id.
	RevokeBiscuit(ctx context.Context, req RevokeBiscuitRequest) error

	// GetBiscuitRevocation reads back what RevokeBiscuit wrote for one
	// revocation id. ErrNotFound when the id is not revoked, or belongs to a
	// capability the caller cannot see.
	GetBiscuitRevocation(ctx context.Context, revocationID []byte) (BiscuitRevocation, error)
}

BiscuitRevocationStore persists revoked Biscuit copies. It is separate from Store so that a consumer without Biscuits implements nothing more.

type BumpRequest

type BumpRequest struct {
	CapabilityID uuid.UUID
	// TenantID is the capability's tenant. Used for attribution (metrics);
	// it is not an authorisation input.
	TenantID uuid.UUID
	// MaxRequests is the capability's own ceiling, from the verified
	// token. 0 = unlimited. Ancestors' ceilings come from their records.
	MaxRequests int64
	// Copies are the presented Biscuit copy's own limits, from the verified
	// token (Capability.Copies), innermost first.
	Copies []CopyCeiling
}

BumpRequest is the input to Meter.Bump.

type CacheOption

type CacheOption func(*cacheConfig)

CacheOption configures a CachedRevocationChecker or a CachedBiscuitRevocationChecker.

func WithCacheClock

func WithCacheClock(now func() time.Time) CacheOption

WithCacheClock replaces time.Now; tests use it to step through expiry.

func WithMaxEntries

func WithMaxEntries(n int) CacheOption

WithMaxEntries bounds the number of cached entries. ≤ 0 keeps the default.

type CachedBiscuitRevocationChecker

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

CachedBiscuitRevocationChecker caches IsBiscuitRevoked answers per token — keyed by its whole list of revocation ids — under the same TTL, bound and sharing of concurrent misses as CachedRevocationChecker, and is cleared the same way. It has no Invalidate: a revoked id is part of the key of every token carrying it, so only Clear reaches them all.

func NewCachedBiscuitRevocationChecker

func NewCachedBiscuitRevocationChecker(upstream BiscuitRevocationLookup, ttl time.Duration, opts ...CacheOption) *CachedBiscuitRevocationChecker

NewCachedBiscuitRevocationChecker wires a cache over an upstream lookup. ttl and the options apply as they do to NewCachedRevocationChecker.

func (*CachedBiscuitRevocationChecker) Clear

func (c *CachedBiscuitRevocationChecker) Clear()

Clear drops every entry; wire it to the same revocation notification as CachedRevocationChecker.Clear.

func (*CachedBiscuitRevocationChecker) IsBiscuitRevoked

func (c *CachedBiscuitRevocationChecker) IsBiscuitRevoked(ctx context.Context, revocationIDs [][]byte) (bool, error)

IsBiscuitRevoked serves a fresh cached answer, or asks upstream once on behalf of every concurrent caller asking about the same token. An upstream error is returned and never cached.

func (*CachedBiscuitRevocationChecker) Sweep

Sweep drops every expired entry and returns how many it dropped.

type CachedRevocationChecker

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

CachedRevocationChecker is a small in-memory cache for IsRevoked answers.

A cached answer about a capability is also an answer about its ancestors, so revoking a parent reaches a child's cached "live" entry only when that entry expires — within the TTL, like any other revocation seen from another process.

Concurrent misses for the same id share one upstream call.

func NewCachedRevocationChecker

func NewCachedRevocationChecker(upstream RevocationLookup, ttl time.Duration, opts ...CacheOption) *CachedRevocationChecker

NewCachedRevocationChecker wires a cache over an upstream lookup. ttl of 0 falls back to DefaultRevocationCacheTTL; ttl < 0 disables caching (every call hits upstream — useful for tests).

func (*CachedRevocationChecker) Clear

func (c *CachedRevocationChecker) Clear()

Clear drops every entry. A consumer that learns of a revocation from elsewhere — a database notification, a message bus — calls it so the next check goes upstream at once instead of waiting out the TTL. It drops everything, not only the revoked id, because an answer cached for a descendant is also an answer about the revoked ancestor.

func (*CachedRevocationChecker) Invalidate

func (c *CachedRevocationChecker) Invalidate(id uuid.UUID)

Invalidate drops a single entry. Other processes catch up within the TTL unless they are told — see Clear, which a consumer wires to a revocation notification so they are.

func (*CachedRevocationChecker) IsRevoked

func (c *CachedRevocationChecker) IsRevoked(ctx context.Context, id uuid.UUID) (bool, error)

IsRevoked serves a fresh cached answer, or asks upstream once on behalf of every concurrent caller asking about the same id. An upstream error is returned to all of them and is never cached.

func (*CachedRevocationChecker) Sweep

func (c *CachedRevocationChecker) Sweep() int

Sweep drops every expired entry and returns how many it dropped. Optional housekeeping — lookups skip stale entries, and every new answer drops the expired ones before it.

type Capability

type Capability struct {
	// ID is the unique identifier (UUIDv7 — sortable by issuance time
	// so per-tenant indexes stay tight).
	ID uuid.UUID

	// Issuer is the service instance that minted the capability. Verifiers
	// accept tokens whose Issuer is in the trusted set.
	Issuer string

	// Subject is the principal the capability authorises.
	Subject Principal

	// Audience pins which services the capability is valid for.
	// Typical values: "data", "admin", "mcp". Verifiers reject tokens
	// whose audience does not include the calling plane.
	Audience []string

	// Caveats is the conjunction of restrictions — every caveat must
	// be satisfied by the request, and child capabilities can only
	// add further caveats, never relax one.
	Caveats Caveats

	// IssuedAt / NotBefore / ExpiresAt are standard JWT-style times.
	IssuedAt  time.Time
	NotBefore time.Time
	ExpiresAt time.Time

	// ParentID, if non-zero, points at the parent capability whose
	// holder delegated this one. Narrowing is checked once, at
	// delegation. At use, the lineage matters twice: revoking any
	// ancestor revokes this capability (Store.IsRevoked answers for the
	// whole chain), and every charge also debits each ancestor, so a
	// parent's budget bounds the spend of everything delegated from it.
	ParentID uuid.UUID

	// Generation is an issuer-assigned counter (1 for a fresh root, 1 for
	// every delegated child) carried in the token for audit. A consumer
	// that rotates a capability may reissue with Generation+1 and revoke
	// the previous one, and compare generations in its own records.
	//
	// It is NOT a fence token: nothing in this package compares it
	// against a stored current value, so an older generation stays usable
	// until it expires or is revoked. Revocation is the mechanism that
	// stops a superseded capability.
	Generation int64

	// ConfirmationJKT, when set, binds the capability to a key: the RFC 7638
	// thumbprint of the public key whose holder alone may present it. Every
	// request must then carry a DPoP proof signed by that key (dpop.go). A
	// delegated child of a bound capability is bound too. Carried in the
	// token as `cnf.jkt`, and absent from tokens that are not bound.
	ConfirmationJKT string

	// BiscuitRoot is set only on the token sealed inside a Biscuit (see
	// Issuer.Biscuit): the base64url Ed25519 public key that roots the
	// Biscuit's signature chain. A token carrying it is refused when
	// presented on its own, so it cannot be lifted out of an attenuated
	// Biscuit to shed the attenuation.
	BiscuitRoot string

	// Copies are the request and budget limits the attenuation blocks of a
	// verified Biscuit set, innermost block first. Each is counted on its
	// own, beside the capability's counters, by every Meter call that is
	// handed them. Set by the verifier only; never part of a token or a
	// stored record.
	Copies []CopyCeiling `json:"-"`
}

Capability is the in-memory representation of a verified token. The wire form is a JWT; this struct is what callers receive from Verifier.Verify and what the Issuer fills before signing.

func Decode

func Decode(token string) (Capability, error)

Decode parses a compact-form token without verifying the signature or applying time / audience / revocation gates. Use only for tooling that displays a token; production code goes through Verifier.Verify, which never reads a claim before the signature has checked out.

type CapabilityLister added in v0.2.0

type CapabilityLister interface {
	ListByPrincipal(ctx context.Context, req ListByPrincipalRequest) ([]Capability, string, error)
}

CapabilityLister lists a principal's capabilities a page at a time. Every Store is one.

type Caveats

type Caveats struct {
	// Ops is the set of operations the capability authorises. The
	// issuer rejects an empty set: a capability that allows nothing is
	// a mistake, not a credential.
	Ops []Op `json:"ops"`

	// ResourcePrefixes restricts the capability to URIs under any prefix
	// in the slice, matched at a "/" segment boundary (MatchResource):
	// "a/b" covers "a/b" and "a/b/c" but not "a/bc". Empty slice =
	// unrestricted within the tenant; an empty ENTRY is invalid.
	ResourcePrefixes []string `json:"resource_prefixes,omitempty"`

	// ResourceURIs pins the capability to specific URIs. Used by
	// hand-off flows where the parent passes exactly the artifacts the
	// child needs.
	ResourceURIs []string `json:"resource_uris,omitempty"`

	// MaxRequests caps the total number of times the capability may
	// be used. 0 = unlimited within the TTL.
	MaxRequests int64 `json:"max_requests,omitzero"`

	// MaxBudgetAmount is the cost budget the capability authorises, in
	// the currency identified by UnitCode, exact to the nano. The Meter
	// counts spend against it; once exhausted, a charge returns
	// ErrBudgetExceeded.
	MaxBudgetAmount Nanos `json:"max_budget,omitzero"`

	// UnitCode pins the currency or unit: an ISO 4217 code (IsUnitCode),
	// XXX ("no currency") for a budget that is not money. Empty value is
	// interpreted as the default ("USD") at the application boundary; the
	// in-memory shape is happy to carry either form.
	UnitCode string `json:"unit_code,omitempty"`

	// AllowTaintedRead permits non-mutating operations on resources the
	// consumer has flagged with prompt-injection / PII / secrets signals.
	// Off by default. Enforced by Caveats.Check, which can only act on a
	// taint signal the consumer supplies in CheckRequest.ResourceTainted
	// — a consumer that tracks no taint flags gets no protection from
	// this caveat, and should say so rather than imply it.
	AllowTaintedRead bool `json:"allow_tainted_read,omitzero"`

	// IdempotencyKeyRequired forces the bearer to send an explicit
	// idempotency key on mutating operations (CheckRequest.Mutating).
	// Enforced by Caveats.Check from CheckRequest.HasIdempotencyKey.
	IdempotencyKeyRequired bool `json:"idempotency_key_required,omitzero"`

	// SourceIPCIDR optionally pins the capability to clients whose
	// observed IP is in the listed CIDRs. Empty = unconstrained.
	// Enforced by Caveats.CheckSource; delegation requires each child
	// network to lie inside a parent network.
	SourceIPCIDR []string `json:"source_ip_cidr,omitempty"`
}

Caveats is a typed bag of restrictions. Empty values are interpreted as "no restriction on that axis"; explicit bounds are AND-combined.

The json tags are the token's wire format (the limes_caveats claim), so renaming a field changes nothing a token carries. A verifier refuses a member it does not know: a caveat it cannot enforce must not pass as one it did.

A child capability may only narrow caveats — set a tighter prefix, drop ops, lower the budget, shorten TTL. The verifier enforces this at delegation time, not just at use time, so attempts to widen are detected at issue and rejected.

func (Caveats) AllowsResource

func (c Caveats) AllowsResource(uri string) bool

AllowsResource reports whether uri is reachable under the resource caveats: unrestricted caveats allow any non-empty uri; restricted ones require an exact ResourceURIs entry or a covering ResourcePrefixes entry. An empty uri, or one with a dot segment (HasDotSegment), is never allowed by restricted caveats — an operation that cannot name what it touches cannot be shown to stay inside the scope.

func (Caveats) Check

func (c Caveats) Check(req CheckRequest) error

Check evaluates every per-operation caveat against req and returns the first violation, as one of the sentinels above, or nil. A declared Effect that Op.ResolveEffect refuses returns ErrEffectConflict, which is a programming error rather than a caveat violation. It is the one definition of what the caveats mean at use time; a consumer that hand-rolls the same checks drifts from it.

The source-IP caveat is per connection rather than per operation, so it is checked separately by CheckSource. Budget and request-count caveats are stateful and enforced by the UsageStore.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/oleg-tkachuk/limes"
)

func main() {
	caveats := limes.Caveats{
		Ops:              []limes.Op{limes.OpGet},
		ResourcePrefixes: []string{"corpus/public/"},
	}

	fmt.Println(caveats.Check(limes.CheckRequest{Op: limes.OpGet, Resource: "corpus/public/report.pdf"}))

	// A prefix matches at a "/" boundary only.
	err := caveats.Check(limes.CheckRequest{Op: limes.OpGet, Resource: "corpus/public-secret/x"})
	fmt.Println(errors.Is(err, limes.ErrResourceNotAllowed), errors.Is(err, limes.ErrCaveatViolation))

	err = caveats.Check(limes.CheckRequest{Op: limes.OpPut, Resource: "corpus/public/report.pdf"})
	fmt.Println(errors.Is(err, limes.ErrOpNotAllowed))
}
Output:
<nil>
true true
true

func (Caveats) CheckSource

func (c Caveats) CheckSource(addr netip.Addr) error

CheckSource evaluates the source-IP caveat against the client address the consumer observed. An unknown address (the zero netip.Addr) fails a constrained capability closed: "we could not tell" is not "it matched".

func (Caveats) RestrictsResources

func (c Caveats) RestrictsResources() bool

RestrictsResources reports whether the caveats confine the capability to a set of resources. False means "any resource within the tenant".

func (Caveats) Validate

func (c Caveats) Validate() error

Validate reports whether the caveat bag is well formed. The issuer runs it before minting anything, root or delegated, so every signed capability carries caveats with one meaning.

type ChargeReceipt

type ChargeReceipt struct {
	// ChargeID identifies the ledger row; pass it to Meter.Refund.
	ChargeID uuid.UUID
	// Spent is the capability's spend as this call returns (its subtree's,
	// when it has delegated children).
	Spent Nanos
	// Replayed reports that the charge was already in the ledger — the same
	// ExternalRef, or a reservation already settled — so this call moved
	// nothing and ChargeID names the earlier charge.
	Replayed bool
	// Overrun reports that the charge crossed at least one ceiling, which
	// only OverrunRecord allows.
	Overrun bool
	// TenantID and UnitCode are the charge's tenant and unit, so a caller
	// holding only a receipt — of a Settle, say, whose request names only the
	// reservation — can attribute it.
	TenantID uuid.UUID
	UnitCode string
}

ChargeReceipt is the result of a committed charge.

type ChargeRecord

type ChargeRecord struct {
	ChargeID     uuid.UUID
	CapabilityID uuid.UUID
	// Amount is what the charge took; Refunded is what has been returned
	// from it since.
	Amount   Nanos
	Refunded Nanos
	UnitCode string
	// ExternalRef is the charge's ChargeRequest.ExternalRef; empty for none.
	ExternalRef string
	// Overrun reports that the charge crossed a ceiling (OverrunRecord).
	Overrun bool
}

ChargeRecord is one charge as the ledger holds it.

type ChargeRequest

type ChargeRequest struct {
	CapabilityID uuid.UUID
	// TenantID is the capability's tenant: its aggregate ceiling applies
	// and the ledger row is filed under it. Required.
	TenantID uuid.UUID
	// Amount is the cost, in UnitCode. Must be finite and ≥ 0.
	Amount Nanos
	// MaxBudget is the capability's own ceiling, from the verified token.
	// 0 = unlimited.
	MaxBudget Nanos
	// UnitCode is the currency or unit; empty means DefaultUnitCode. No
	// conversion happens: a charge in a unit other than one a counter it
	// moves already counts in — the capability's, an ancestor's, the
	// tenant's — returns ErrUnitCodeMismatch and moves nothing. So does a
	// reservation.
	UnitCode string
	// Op and Actor are stamped on the ledger row for attribution only;
	// both may be empty and neither affects enforcement.
	Op    string
	Actor string
	// Copies are the presented Biscuit copy's own limits (Capability.Copies).
	Copies []CopyCeiling
	// ExternalRef names the cost in the consumer's own records — the id of
	// the call or the event that reported it — and makes the charge
	// idempotent on (CapabilityID, ExternalRef). Empty: every call charges.
	// At most MaxExternalRefBytes long.
	ExternalRef string
	// Overrun is what happens when Amount would cross a ceiling.
	Overrun OverrunPolicy
}

ChargeRequest is the input to Meter.Charge.

type CheckRequest

type CheckRequest struct {
	// Op is the operation being performed.
	Op Op
	// Effect is what Op does to state, as the server declares it. Leave it
	// EffectUnspecified for a built-in operation; see Op.ResolveEffect.
	Effect Effect
	// Resource is the URI the operation touches. Leave it empty only for an
	// operation that is not bound to one resource; a resource-restricted
	// capability refuses such an operation (see Caveats.AllowsResource).
	// For an operation over a set — a listing, a batch — pass the URI or
	// prefix that bounds the whole set.
	Resource string
	// HasIdempotencyKey reports whether the request carried an idempotency
	// key. Consulted only when the caveats require one for a mutating
	// operation.
	HasIdempotencyKey bool
	// ResourceTainted reports whether the consumer has flagged the resource
	// with a prompt-injection / PII / secrets signal. A consumer with no
	// taint tracking leaves it false — AllowTaintedRead then protects
	// nothing, which is the consumer's to disclose.
	ResourceTainted bool
}

CheckRequest describes one operation a bearer is attempting, in the terms the caveats are written in. The consumer fills it from its own request.

func (CheckRequest) Mutating

func (req CheckRequest) Mutating() (bool, error)

Mutating reports whether the operation may change state, resolving its declared Effect. A consumer that gates anything else on the operation's effect — a lookup it runs only for reads — asks here, so that it and Check cannot disagree.

type CopyCeiling

type CopyCeiling struct {
	// RevocationID is the block's revocation id.
	RevocationID []byte
	// MaxRequests bounds the requests made with the copy; 0 = no limit here.
	MaxRequests int64
	// MaxBudget bounds the copy's spend, in the capability's unit; 0 = no
	// limit here.
	MaxBudget Nanos
}

CopyCeiling is a limit one attenuation block of a Biscuit set on the copy it ends and every copy attenuated from it, all of which carry the block's revocation id. Its counters are keyed by that id, so siblings attenuated apart count apart, while the capability's own counters still bound them all together.

type CopyUsage

type CopyUsage struct {
	RevocationID   []byte
	CapabilityID   uuid.UUID
	RequestCount   int64
	SpentAmount    Nanos
	ReservedAmount Nanos
}

CopyUsage is the counters of one Biscuit copy with limits of its own (see Meter, "Copies of a Biscuit"): the copy and every copy attenuated from it.

type CopyUsageReader

type CopyUsageReader interface {
	// CopyUsage returns the counters of each copy named that has been used,
	// in no particular order; a copy never used has no counters and is
	// absent from the result.
	CopyUsage(ctx context.Context, revocationIDs [][]byte) ([]CopyUsage, error)
}

CopyUsageReader reads the counters of Biscuit copies. It is separate from Meter so that a Meter which does not count copies implements nothing more.

type DPoPJWK

type DPoPJWK struct {
	Kty string `json:"kty"`
	Crv string `json:"crv"`
	X   string `json:"x"`
	Y   string `json:"y,omitempty"`
}

DPoPJWK is the public key a proof carries in its header: an Ed25519 (OKP) or P-256 (EC) key.

func PublicJWK

func PublicJWK(pub crypto.PublicKey) (DPoPJWK, error)

PublicJWK describes pub, an ed25519.PublicKey or a P-256 *ecdsa.PublicKey, as a JWK.

func (DPoPJWK) Thumbprint

func (j DPoPJWK) Thumbprint() (string, error)

Thumbprint is the key's RFC 7638 SHA-256 thumbprint, base64url — the value a key-bound capability carries as its ConfirmationJKT.

type DPoPNonces added in v0.3.0

type DPoPNonces interface {
	// Nonce returns a nonce to send in the DPoP-Nonce header.
	Nonce(ctx context.Context) (string, error)
	// Valid reports whether nonce is one Nonce issued that is still current.
	Valid(ctx context.Context, nonce string) bool
}

DPoPNonces issues the nonces a verifier hands out and recognises them when a proof carries one back.

type DPoPProof added in v0.3.0

type DPoPProof struct {
	// Proof is the DPoP header's value; empty when the request had none.
	Proof string
	// Method is the HTTP method, e.g. "POST".
	Method string
	// URL is the request URL, as the server saw it (see
	// DPoPVerifier.MatchPathOnly).
	URL string
}

DPoPProof is a request's proof of possession and the request it must match: what VerifyRequest.DPoP carries for a key-bound capability.

type DPoPProofRequest added in v0.3.0

type DPoPProofRequest struct {
	// Method and URL are the request's.
	Method string
	URL    string
	// Token is the capability the request presents.
	Token string
	// Nonce is the last DPoP-Nonce the server sent, empty before it has
	// sent one.
	Nonce string
	// Now is the proof's iat.
	Now time.Time
}

DPoPProofRequest is the request a proof is signed for.

type DPoPVerifier

type DPoPVerifier struct {
	// Window bounds |now − iat|. Default DefaultDPoPWindow.
	Window time.Duration
	// Replay refuses a jti seen before. Required: without it a captured
	// proof is good for the whole window, and NewStandardVerifier refuses
	// the configuration.
	Replay ReplayCache
	// Now is the clock; default the verifier's.
	Now func() time.Time
	// Nonces, when set, requires every proof to carry a nonce it issued
	// (RFC 9449 §8); a proof without one is refused with
	// ErrDPoPNonceRequired, and StandardVerifier.DPoPNonce gives the nonce
	// to send back.
	Nonces DPoPNonces
	// MatchPathOnly compares only the path of DPoPProof.URL, and lets the
	// proof's path carry extra leading segments — for a server behind a
	// proxy, which cannot know the scheme, host or mount prefix its clients
	// address it by.
	MatchPathOnly bool
}

DPoPVerifier is how a StandardVerifier checks the proofs of key-bound capabilities (VerifierConfig.DPoP).

type DelegateRequest

type DelegateRequest struct {
	Parent   Capability
	Subject  Principal
	Audience []string
	Caveats  Caveats
	// InheritCaveats copies the parent's caveats verbatim and ignores
	// Caveats. It must be asked for: an earlier version inherited silently
	// whenever Caveats.Ops was empty, which replaced every other field the
	// caller HAD set — a budget of 2 became the parent's 25 without a word.
	InheritCaveats bool
	TTL            time.Duration
	NotBefore      time.Time
	// ConfirmationJKT binds the child to a key — typically the sub-agent's.
	// Empty keeps the parent's binding, if it has one: a bound parent's
	// child is always bound.
	ConfirmationJKT string
}

DelegateRequest narrows from a parent capability the caller already holds. The issuer enforces narrowing (Narrows) before persisting so a runtime widening attempt fails before any token is emitted.

type Effect

type Effect uint8

Effect is what an operation does to state. Check uses it twice: a mutating operation may need an idempotency key (Caveats.IdempotencyKeyRequired), and a read may be refused on a tainted resource (Caveats.AllowTaintedRead).

The server that performs the operation declares the effect, from the same place it defines the operation (a method option, a tool registry), and never from anything the bearer sends.

const (
	// EffectUnspecified takes the operation's default: the fixed effect of a
	// built-in operation, and EffectWrite for a consumer-defined one, whose
	// effect this package cannot know — so an undeclared operation errs
	// towards the caveats that tighten mutations.
	EffectUnspecified Effect = iota
	// EffectRead leaves state unchanged.
	EffectRead
	// EffectWrite may change state.
	EffectWrite
)

func (Effect) String

func (e Effect) String() string

String names the effect for logs and errors.

type ErrorResponse added in v0.3.0

type ErrorResponse struct {
	// Status is the HTTP status.
	Status int
	// Challenges are the WWW-Authenticate values, one per scheme; none for a
	// refusal that is not about authentication.
	Challenges []string
	// DPoPNonce is the nonce to send in the DPoP-Nonce header, when the
	// refusal asks for one.
	DPoPNonce string
	// Problem is the body.
	Problem Problem
}

ErrorResponse is how to answer a refused request.

type HMACNonces added in v0.3.0

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

HMACNonces is a DPoPNonces that keeps no state: a nonce is its issuance time and an HMAC of it, so every replica holding the same keys issues and accepts the same nonces. Rotate by putting the new key first: it issues, and the old ones are still accepted until their nonces expire.

func NewHMACNonces added in v0.3.0

func NewHMACNonces(lifetime time.Duration, keys ...[]byte) (*HMACNonces, error)

NewHMACNonces builds an HMACNonces. keys[0] signs new nonces; every key checks. Each key is at least HMACNonceKeyBytes long; lifetime ≤ 0 is DefaultDPoPNonceLifetime.

func (*HMACNonces) Nonce added in v0.3.0

func (n *HMACNonces) Nonce(context.Context) (string, error)

Nonce implements DPoPNonces.

func (*HMACNonces) Valid added in v0.3.0

func (n *HMACNonces) Valid(_ context.Context, nonce string) bool

Valid implements DPoPNonces: a nonce one of the keys signed, issued no longer than the lifetime ago and not in the future.

type IssueRequest

type IssueRequest struct {
	Subject Principal
	// IssuedBy is the principal REQUESTING the capability — an operator for a
	// root issuance, the parent's holder for a delegation. Distinct from
	// Subject, which is who the capability authorises. Required: a capability
	// nobody requested cannot be audited, and the store rejects an empty one.
	IssuedBy   Principal
	Audience   []string
	Caveats    Caveats
	TTL        time.Duration // 0 → DefaultTTL
	NotBefore  time.Time     // zero → now
	Generation int64         // 0 → 1
	// ConfirmationJKT binds the capability to the key with this RFC 7638
	// thumbprint (KeyThumbprint); empty issues a bearer capability.
	ConfirmationJKT string
}

IssueRequest is the input shape for Issuer.Issue. All fields except TTL / NotBefore / Generation are required; the issuer fills in ID, IssuedAt, ExpiresAt, Issuer, Generation when not supplied. Caveats are validated (Caveats.Validate) before anything is persisted.

type Issuer

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

Issuer mints capabilities. Two responsibilities, in this order:

  1. Validate the request (caveats sane, parent narrowing if delegated).
  2. Persist the audit row, then sign and return the token.

The persist-then-sign order is deliberate. If we signed first and the DB write failed, an emitted token would have no record — verifiers that consult capability_records (admin tooling) would see a forged- looking token, and a missing row prevents Revoke from doing anything useful. Persist-first means the token in the caller's hand is always reflected in storage; a failed sign just means the row is unused (cleaned up by housekeeping at the natural expiry).

func NewIssuer

func NewIssuer(cfg IssuerConfig) (*Issuer, error)

NewIssuer validates wiring and returns a ready Issuer.

func (*Issuer) Biscuit

func (i *Issuer) Biscuit(ctx context.Context, c Capability) (string, error)

Biscuit returns the Biscuit form of a capability this issuer issued: the same capability — same ID, caveats, expiry and binding, revoked together with it — in a token its holder can attenuate offline. One copy can also be revoked on its own; see BiscuitRevocationStore.

func (*Issuer) Delegate

func (i *Issuer) Delegate(ctx context.Context, req DelegateRequest) (Capability, string, error)

Delegate mints a child capability narrower than the supplied parent. The parent must already be a verified Capability — callers run it through Verifier.Verify first; the Issuer trusts its in-memory shape. It does not trust it to be current, though: a parent that has expired, or that the store reports revoked (itself or any ancestor), delegates nothing; nor does a Biscuit copy with limits of its own (Parent.Copies), which returns ErrDelegationTooWide.

Example
package main

import (
	"context"
	"crypto/ed25519"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

	"github.com/oleg-tkachuk/limes"
	"github.com/oleg-tkachuk/limes/memstore"
)

// The issuer's name, and the audience every example's tokens are minted for.
const (
	exampleIssuerName = "example-issuer"
	exampleAudience   = "my-service"
)

// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
	issuer   *limes.Issuer
	verifier *limes.StandardVerifier
	records  *memstore.Store[struct{}]
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	kid, pub, priv, err := limes.GenerateEd25519Keypair()
	if err != nil {
		log.Fatal(err)
	}
	records := memstore.New[struct{}]()
	signer, err := limes.NewEd25519Signer(kid, priv)
	if err != nil {
		log.Fatal(err)
	}
	issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewStandardVerifier(limes.VerifierConfig{
		Keys:               limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               &limes.DPoPVerifier{Replay: limes.NewMemoryReplayCache(0)},
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: memstore.NewUsage[struct{}](records), tenantID: uuid.New(),
	}
}

// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
	c, token, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
		},
		Audience: []string{exampleAudience},
		TTL:      15 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet, limes.OpShare},
			ResourcePrefixes: []string{"corpus/public/"},
			MaxRequests:      500,
			MaxBudgetAmount:  limes.MustParseAmount("25.00"),
			UnitCode:         "USD",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	parent, _ := s.issue()

	worker := limes.Principal{Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "sub-worker"}
	child, _, err := s.issuer.Delegate(ctx, limes.DelegateRequest{
		Parent:  parent,
		Subject: worker,
		TTL:     2 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet},
			ResourcePrefixes: []string{"corpus/public/2026/"},
			MaxRequests:      50,
			MaxBudgetAmount:  limes.MustParseAmount("2.00"),
			UnitCode:         "USD",
		},
	})
	fmt.Println(err, child.ParentID == parent.ID)

	// Asking for more than the parent holds is refused.
	_, _, err = s.issuer.Delegate(ctx, limes.DelegateRequest{
		Parent:  parent,
		Subject: worker,
		TTL:     time.Minute,
		Caveats: limes.Caveats{
			Ops:             []limes.Op{limes.OpGet},
			MaxBudgetAmount: limes.MustParseAmount("999"),
			UnitCode:        "USD",
		},
	})
	fmt.Println(errors.Is(err, limes.ErrDelegationTooWide))
}
Output:
<nil> true
true

func (*Issuer) Issue

func (i *Issuer) Issue(ctx context.Context, req IssueRequest) (Capability, string, error)

Issue mints a top-level capability. Generation defaults to 1; see Capability.Generation for what it does and does not promise.

Returns the typed Capability and the compact-form token in one call — callers usually need both: the token to ship to the agent, the struct to log / display.

Example
package main

import (
	"context"
	"crypto/ed25519"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

	"github.com/oleg-tkachuk/limes"
	"github.com/oleg-tkachuk/limes/memstore"
)

// The issuer's name, and the audience every example's tokens are minted for.
const (
	exampleIssuerName = "example-issuer"
	exampleAudience   = "my-service"
)

func main() {
	ctx := context.Background()
	kid, pub, priv, _ := limes.GenerateEd25519Keypair()

	records := memstore.New[struct{}]() // your Store
	signer, _ := limes.NewEd25519Signer(kid, priv)
	issuer, _ := limes.NewIssuer(limes.IssuerConfig{
		Signer: signer, Store: records, IssuerName: exampleIssuerName,
	})
	verifier, _ := limes.NewStandardVerifier(limes.VerifierConfig{
		Keys:           limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
		Revocations:    records,
		TrustedIssuers: []string{exampleIssuerName},
	})

	c, token, err := issuer.Issue(ctx, limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalAgent, TenantID: uuid.New(), Subject: "research-orchestrator",
		},
		Audience: []string{exampleAudience},
		TTL:      15 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet},
			ResourcePrefixes: []string{"corpus/public/"},
			MaxBudgetAmount:  limes.MustParseAmount("25.00"),
			UnitCode:         "USD",
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	got, err := verifier.Verify(ctx, limes.VerifyRequest{Token: token, Audience: exampleAudience})
	fmt.Println(err == nil, got.ID == c.ID)
	_, err = verifier.Verify(ctx, limes.VerifyRequest{Token: token, Audience: "another-service"})
	fmt.Println(errors.Is(err, limes.ErrAudienceMismatch))
}
Output:
true true
true

type IssuerConfig

type IssuerConfig struct {
	// Signer mints tokens. Production wraps a KMS-held private key;
	// dev / tests use NewEd25519Signer with a local generated key.
	Signer Signer

	// Store persists issuance + revocation rows.
	Store Store

	// IssuerName is the value placed in the `iss` claim. Verifiers
	// require this to be in their TrustedIssuers list.
	IssuerName string

	// DefaultTTL caps the lifetime when IssueRequest.TTL is zero.
	// Default 15 minutes when zero — short enough that revocation
	// propagation latency rarely matters.
	DefaultTTL time.Duration

	// Now is the clock; tests override.
	Now func() time.Time
}

IssuerConfig is the wiring for Issuer.New.

type JWK

type JWK struct {
	Kty string `json:"kty"`           // "OKP"
	Crv string `json:"crv"`           // "Ed25519"
	Kid string `json:"kid"`           // matches jwtHeader.Kid
	Alg string `json:"alg,omitempty"` // "Ed25519" (RFC 9864); "EdDSA" is read
	X   string `json:"x"`             // base64url-encoded raw public key
}

JWK is one entry in the JWKS — exactly the fields needed to verify an Ed25519 signature; we do not embed `use` or `key_ops` because the kid + alg pair fully constrains use here.

type JWKSDocument

type JWKSDocument struct {
	Keys []JWK `json:"keys"`
}

JWKSDocument is the on-the-wire shape served at the issuer's JWKS endpoint (typically `/.well-known/jwks.json` on the admin plane). Verifiers in other planes / pods fetch and cache this document with a short TTL so capability verification stays local without an extra RPC per request.

Format: RFC 7517 with kty=OKP and crv=Ed25519 (RFC 8037), alg=Ed25519 (RFC 9864).

Serve capability keys from their own document, separate from any IdP JWKS: capability tokens use a distinct algorithm, issuer set and rotation cadence, and one shared document would conflate two trust roots.

type KeyResolver

type KeyResolver interface {
	// PublicKey returns the Ed25519 public key for the given kid.
	// Returns ErrUnknownKID if the key set does not contain the kid;
	// the verifier maps that to ErrInvalidSignature so an unknown
	// kid never short-circuits to "trusted".
	PublicKey(ctx context.Context, kid string) (ed25519.PublicKey, error)
}

KeyResolver resolves a JWKS `kid` to the public key the verifier should use. StaticKeyResolver holds keys in memory; RemoteJWKSResolver fetches them from an issuer's JWKS endpoint with a cache.

type ListByPrincipalRequest

type ListByPrincipalRequest struct {
	// TenantID, PrincipalType and Subject name the principal; all three are
	// required (Validate).
	TenantID      uuid.UUID
	PrincipalType PrincipalType
	Subject       string
	// IncludeExpired and IncludeRevoked also list capabilities that have
	// expired, or that are revoked themselves (not through an ancestor).
	IncludeExpired bool
	IncludeRevoked bool
	// Cursor is the one the previous page returned; empty for the first.
	Cursor string
	// Limit is the page size; see PageLimit.
	Limit int32
}

ListByPrincipalRequest is the input shape for Store.ListByPrincipal.

func (ListByPrincipalRequest) Validate

func (r ListByPrincipalRequest) Validate() error

Validate reports whether the request names a principal. Every error matches ErrInvalidRequest.

type ListTenantBudgetsRequest

type ListTenantBudgetsRequest struct {
	ThresholdPct    float64
	UnlimitedOnly   bool
	ExcludeInactive bool
	Limit           int32
	// Cursor is the one the previous page returned; empty for the first.
	Cursor string
}

ListTenantBudgetsRequest is the input shape for UsageStore.ListTenantBudgets.

type MemoryReplayCache

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

MemoryReplayCache is a bounded in-process ReplayCache. A deployment with several replicas behind a balancer that does not pin a client to one replica can see a proof replayed on another replica within the window; keep the window short, or back ReplayCache with a shared store.

func NewMemoryReplayCache

func NewMemoryReplayCache(maxEntries int) *MemoryReplayCache

NewMemoryReplayCache bounds the cache at maxEntries (≤ 0: 100 000).

func (*MemoryReplayCache) Seen

func (c *MemoryReplayCache) Seen(_ context.Context, jti string, expires time.Time) bool

Seen implements ReplayCache. Expired ids are dropped as they lapse, soonest first, so a full cache costs a lookup rather than a scan. When it is full of live ids, the new id is refused as a replay rather than accepted unrecorded — a full cache errs towards refusing.

type Meter

type Meter[TX any] interface {
	// Bump increments the request counter of the capability and of
	// each ancestor, and returns the capability's new count. When any
	// ceiling in the chain would be crossed it returns
	// ErrRequestLimitExceeded and nothing is mutated.
	Bump(ctx context.Context, req BumpRequest) (newCount int64, err error)

	// Charge adds req.Amount to the spend counters of the capability, of
	// each ancestor and of the tenant aggregate, writes one charges-ledger
	// row, and runs onCharged — ALL atomically. Checks, in order:
	//
	//   1. Each Biscuit copy's own budget (req.Copies), innermost first →
	//      ErrBudgetExceeded.
	//   2. The capability's own ceiling (req.MaxBudget, from the verified
	//      token) → ErrBudgetExceeded.
	//   3. Each ancestor's ceiling → ErrBudgetExceeded.
	//   4. The tenant aggregate ceiling → ErrTenantBudgetExceeded.
	//
	// Any rejection, or an error from onCharged, leaves every counter and
	// the ledger untouched. onCharged runs inside the charge, atomically
	// with it, and receives the consumer's transaction handle, so a side
	// effect such as an outbox write commits with the spend. Pass nil to
	// skip it. It must not call back into the Meter: an implementation may
	// hold its counters while onCharged runs, so a nested call can wait on
	// itself.
	//
	// The returned receipt carries the ledger row's ID; Refund takes it.
	//
	// With req.ExternalRef set, Charge is idempotent on (CapabilityID,
	// ExternalRef): a charge whose pair is already in the ledger moves
	// nothing, does not run onCharged, and returns that charge's receipt
	// with Replayed set. The pair names one cost: arriving again with a
	// different Amount, UnitCode or TenantID it returns
	// ErrExternalRefConflict and moves nothing. With req.Overrun = OverrunRecord, a charge that
	// would cross a ceiling is committed anyway and the receipt reports it
	// (ChargeReceipt.Overrun); see OverrunPolicy.
	Charge(ctx context.Context, req ChargeRequest, onCharged func(ctx context.Context, tx TX) error) (ChargeReceipt, error)

	// Refund returns spend from one recorded charge to every counter that
	// charge debited (capability, ancestors, tenant aggregate), and records
	// the refund against the charge. req.Amount = 0 refunds whatever
	// remains, which makes a full refund idempotent; a partial refund that
	// would take the total refunded past the charge returns
	// ErrRefundExceedsCharge and changes nothing. An unknown charge ID
	// returns ErrChargeNotFound. The receipt carries the amount actually
	// refunded.
	//
	// A charge counted in a tenant accounting period that has since been
	// closed (SetTenantBudget with ResetSpend) is refunded to the
	// capability, its ancestors and its copies, but not to the tenant
	// aggregate: the current period never counted it.
	//
	// For a cost only known afterwards, prefer Reserve/Settle: a
	// reservation lapses on its own if the caller never settles it, where
	// an estimate charged up front stays charged.
	Refund(ctx context.Context, req RefundRequest) (RefundReceipt, error)

	// Reserve holds req.Amount against the capability, each ancestor and
	// the tenant aggregate, checking every ceiling against spend plus
	// what is already held. Holds count against later charges and
	// reservations exactly as spend does, until the reservation is
	// settled, released or expires. Rejections return the Charge
	// sentinels and hold nothing.
	Reserve(ctx context.Context, req ReserveRequest) (Reservation, error)

	// Settle ends a reservation and charges the actual cost in its place,
	// atomically: the hold is released and req.Amount is charged as
	// Charge would, with onCharged run inside. The actual cost may be
	// below, equal to or above the hold; above it, the excess must fit
	// every ceiling, or Settle returns the Charge sentinel and the
	// reservation stays in place for the caller to settle lower or
	// release — unless req.Overrun is OverrunRecord, when the cost is
	// charged anyway and the receipt reports it.
	//
	// Settle is idempotent on the reservation: settling one already
	// settled moves nothing, does not run onCharged, and returns the
	// receipt of the charge that settled it, with Replayed set. An unknown,
	// released or expired reservation returns ErrReservationNotFound, and so
	// does the zero id, which names none — never a direct charge.
	Settle(ctx context.Context, req SettleRequest, onCharged func(ctx context.Context, tx TX) error) (ChargeReceipt, error)

	// GetCharge returns one charge of the ledger, by the ChargeID its
	// receipt carried — the id Refund takes. ErrChargeNotFound when there
	// is none.
	GetCharge(ctx context.Context, chargeID uuid.UUID) (ChargeRecord, error)

	// ChargeByRef returns the charge a capability took under externalRef
	// (ChargeRequest.ExternalRef), so a reporter can reconcile its own
	// records with the ledger. ErrChargeNotFound when there is none.
	ChargeByRef(ctx context.Context, capID uuid.UUID, externalRef string) (ChargeRecord, error)

	// GetReservation returns a reservation still holding budget — open,
	// or expired and not yet released, since until ReleaseExpired runs it
	// still counts (ExpiresAt tells which). ErrReservationNotFound once it
	// is settled or released.
	GetReservation(ctx context.Context, reservationID uuid.UUID) (Reservation, error)

	// ListReservations returns every reservation GetReservation would
	// return that was made against the capability itself — not its
	// delegated children — soonest to expire first.
	ListReservations(ctx context.Context, capID uuid.UUID) ([]Reservation, error)

	// Release ends a reservation without charging. Idempotent: releasing
	// one that is gone is a no-op.
	Release(ctx context.Context, reservationID uuid.UUID) error

	// GetUsage returns the current snapshot. Returns ErrUsageNotFound when no
	// row exists for the capability — typically means it's never been
	// used (no requests, no charges). For a capability with delegated
	// children the counters include the children's usage.
	GetUsage(ctx context.Context, capID uuid.UUID) (Usage, error)
}

Meter enforces the MaxRequests and MaxBudgetAmount caveats and keeps the charges ledger. Separated from Store so the issuance / revocation surface stays focused on identity, not runtime state.

Delegation trees

A capability's counters cover its whole subtree: Bump and Charge apply to the capability AND to every ancestor in its delegation chain, and each ancestor's own ceiling — read from its stored record, not from the caller — must admit the increment. So an orchestrator holding 25.00 that delegates 2.00 to each of a hundred workers can still spend 25.00 in total, not 200.00. Narrowing at delegation bounds each child; this bounds the sum. An ancestor whose record is gone (purged after expiry) no longer bounds anything — its children expired with it, since a child cannot outlive its parent.

Copies of a Biscuit

A Biscuit's holder can give a copy limits of its own (CopyCeiling), which arrive as Copies on BumpRequest, ChargeRequest and ReserveRequest. Each is counted under its revocation id, apart from its siblings, and checked before the capability's ceilings; the capability's counters still take every copy's requests and spend. A charge or reservation records the copies it debited, so Refund, Settle, Release and ReleaseExpired return to them without being handed them again. A Meter that does not count copies must not be paired with a verifier that sets VerifierConfig.MeterCopies.

All methods are concurrency-safe. A rejected call mutates nothing, so a retry after rejection is safe.

type MeteringOption added in v0.3.0

type MeteringOption func(*meteringConfig)

MeteringOption configures WithMetering.

func WithMeterProvider added in v0.3.0

func WithMeterProvider(p metric.MeterProvider) MeteringOption

WithMeterProvider creates the instruments on p rather than on the global provider (otel.GetMeterProvider).

func WithTenantAttribute added in v0.3.0

func WithTenantAttribute() MeteringOption

WithTenantAttribute labels every measurement with tenant_id. Off by default: it multiplies each series by the number of tenants.

type Nanos

type Nanos int64

Nanos is an amount of a currency or metering unit (see UnitCode) in billionths of the unit: 1.5 USD is 1_500_000_000. Every amount the module counts — a charge, a ceiling, a hold, a refund — is Nanos, so spend sums, compares and refunds exactly, however many charges it takes. Float64 appears only at the edges, for display (Float64) and for a caller whose prices arrive as floats (AmountFromFloat).

In JSON, and so in a capability token, an amount is a decimal number of units in a string ("max_budget": "1.5"), written and read without a float64 in between.

const MaxNanos Nanos = 999_999_999_999_999_999

MaxNanos is the largest amount the module counts: just under a billion units, which is what a column of numeric(18,9) holds. Bounding every amount and counter by it leaves the sum of any two inside int64.

func AmountFromFloat

func AmountFromFloat(f float64) (Nanos, error)

AmountFromFloat converts an amount held as a float64 to Nanos, rounding to the nearest nano, halves to even. It reads the float as the shortest decimal that identifies it — 0.1 as "0.1", the sum 0.1+0.2 as "0.30000000000000004" — so an amount written with at most nine decimals and fifteen significant digits converts exactly. It refuses what ValidateAmount refuses, and NaN and the infinities.

func MustParseAmount

func MustParseAmount(s string) Nanos

MustParseAmount is ParseAmount for an amount written into the program, a price or a test value: it panics on one ParseAmount refuses.

func ParseAmount

func ParseAmount(s string) (Nanos, error)

ParseAmount reads a decimal amount of units — "0.35", "25", "1e-7" — into Nanos exactly. It refuses a negative amount, one finer than a nano and one beyond MaxNanos, rather than rounding any of them.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/oleg-tkachuk/limes"
)

func main() {
	a, _ := limes.ParseAmount("0.1")
	b, _ := limes.ParseAmount("0.2")
	fmt.Println(a+b, int64(a+b))

	// Finer than a nano is refused rather than rounded.
	_, err := limes.ParseAmount("0.0000000001")
	fmt.Println(errors.Is(err, limes.ErrInvalidAmount))
}
Output:
0.3 300000000
true

func (Nanos) Float64

func (n Nanos) Float64() float64

Float64 is n in units, the nearest float64. For display and metrics only: never count with it.

func (Nanos) MarshalJSON

func (n Nanos) MarshalJSON() ([]byte, error)

MarshalJSON writes n as a JSON string holding a decimal number of units, "1.5": a string, so that a reader whose numbers are float64 — JavaScript's JSON.parse among them — cannot round it.

func (Nanos) MarshalText

func (n Nanos) MarshalText() ([]byte, error)

MarshalText writes n as a decimal number of units, as String does.

func (Nanos) String

func (n Nanos) String() string

String writes n as a decimal number of units with no trailing zeros: "1.5", "0.000000001", "25".

func (*Nanos) UnmarshalJSON

func (n *Nanos) UnmarshalJSON(b []byte) error

UnmarshalJSON reads what MarshalJSON writes, exactly, as ParseAmount does: a JSON string, never a number.

func (*Nanos) UnmarshalText

func (n *Nanos) UnmarshalText(b []byte) error

UnmarshalText reads a decimal number of units exactly, as ParseAmount does: configuration that carries an amount — a YAML scalar, an environment variable — reads it without a float64 in between.

type Op

type Op string

Op is an operation an agent may perform. The package defines a small built-in set (below); a consumer adds its own as namespaced names of the form "<namespace>:<name>" — "tool:search", "mcp:github/create_issue" — which Op.Validate accepts. A consumer-defined operation is treated as state-changing unless the server declares otherwise (Effect). Fine-grained method gating can still layer admin-authored policy on top.

const (
	OpGet     Op = "get"
	OpPut     Op = "put"
	OpList    Op = "list"
	OpDelete  Op = "delete"
	OpPresign Op = "presign"
	OpTag     Op = "tag"
	OpSearch  Op = "search"
	OpEmbed   Op = "embed"
	OpShare   Op = "share"  // delegate a sub-capability to another agent
	OpManage  Op = "manage" // admin-tier (tenant CRUD, policy authoring)
)

func (Op) Builtin

func (op Op) Builtin() bool

Builtin reports whether op is one of the operations this package defines.

func (Op) Mutating

func (op Op) Mutating() bool

Mutating reports whether op may change state when no effect is declared for it (see Op.ResolveEffect). Built-in read operations are not mutating; every other operation is. OpPresign is read-only by itself — what a presigned URL may do is gated by the get/put assertion that accompanies it.

func (Op) ResolveEffect

func (op Op) ResolveEffect(declared Effect) (Effect, error)

ResolveEffect returns the effect Check applies to op, given the effect the consumer declared for it. A built-in operation keeps its own effect, and a declaration may only repeat it; a consumer-defined operation takes the declaration, or EffectWrite when there is none.

func (Op) Validate

func (op Op) Validate() error

Validate reports whether op is well formed: a built-in operation, or a namespaced one of the form "<namespace>:<name>" made of printable, non-space ASCII.

type OverrunPolicy

type OverrunPolicy uint8

OverrunPolicy is what a charge does when its amount would cross a ceiling — a Biscuit copy's, the capability's, an ancestor's or the tenant's.

const (
	// OverrunReject refuses the charge with the ceiling's sentinel and
	// changes nothing. Use it for a cost not yet incurred: the refusal is
	// what stops it.
	OverrunReject OverrunPolicy = iota
	// OverrunRecord commits the charge past the ceiling and reports it in
	// ChargeReceipt.Overrun. Use it for a cost already incurred — reported
	// after the fact, out of the request path — where refusing would only
	// leave the ledger short of what was spent. The crossed ceiling then
	// refuses every later charge and reservation under OverrunReject.
	OverrunRecord
)

func (OverrunPolicy) Validate

func (p OverrunPolicy) Validate() error

Validate reports whether p is an OverrunPolicy this package defines. Implementations call it before touching state, so an unknown policy is never read as either one.

type Principal

type Principal struct {
	// Type discriminates the principal kind.
	Type PrincipalType `json:"type"`
	// TenantID is required for every principal — capability without
	// tenant scope is malformed and the verifier rejects it.
	TenantID uuid.UUID `json:"tenant_id"`
	// Subject is the stable identity (user UUID, agent run ID, service
	// account name). Format depends on Type.
	Subject string `json:"subject"`
	// Agent fields are populated only for Type == PrincipalAgent.
	Agent *AgentPrincipal `json:"agent,omitzero"`
}

Principal identifies the subject the capability is issued to. Its json tags, and AgentPrincipal's, are the token's wire format (the limes_principal claim). Agent principals have a richer shape than service principals: they carry the run / tool-call lineage that audit and cost attribution need.

type PrincipalType

type PrincipalType string

PrincipalType discriminates Principal.Type.

const (
	PrincipalUser    PrincipalType = "user"
	PrincipalAgent   PrincipalType = "agent"
	PrincipalService PrincipalType = "service"
)

type Problem added in v0.3.0

type Problem struct {
	// Type is a URI naming the kind of problem.
	Type string `json:"type"`
	// Title is a short, fixed summary of the kind.
	Title string `json:"title"`
	// Status repeats the HTTP status.
	Status int `json:"status"`
	// Detail explains this occurrence, in terms safe to show the client.
	Detail string `json:"detail,omitempty"`
}

Problem is an RFC 9457 problem details object.

type Reason

type Reason string

Reason is a stable, machine-readable name for why a presented capability was refused. The sentinels stay the way to branch in Go; a Reason is what a consumer puts on the wire, so that a client in any language tells "this capability is spent" from "this request was malformed" without reading a message. The values are never renamed; new ones may be added.

const (
	ReasonInvalidSignature       Reason = "invalid_signature"
	ReasonKeysUnavailable        Reason = "keys_unavailable"
	ReasonExpired                Reason = "expired"
	ReasonNotYetValid            Reason = "not_yet_valid"
	ReasonRevoked                Reason = "revoked"
	ReasonAudienceMismatch       Reason = "audience_mismatch"
	ReasonInvalidBiscuit         Reason = "invalid_biscuit"
	ReasonProofRequired          Reason = "proof_required"
	ReasonProofInvalid           Reason = "proof_invalid"
	ReasonProofReplayed          Reason = "proof_replayed"
	ReasonOpNotAllowed           Reason = "op_not_allowed"
	ReasonResourceNotAllowed     Reason = "resource_not_allowed"
	ReasonSourceNotAllowed       Reason = "source_not_allowed"
	ReasonIdempotencyKeyRequired Reason = "idempotency_key_required"
	ReasonTaintedReadNotAllowed  Reason = "tainted_read_not_allowed"
	ReasonRequestLimitExceeded   Reason = "request_limit_exceeded"
	ReasonBudgetExceeded         Reason = "budget_exceeded"
	ReasonTenantBudgetExceeded   Reason = "tenant_budget_exceeded"
)

The refusals a verifier, Caveats.Check or a Meter returns for a presented capability.

func ReasonOf

func ReasonOf(err error) (Reason, bool)

ReasonOf returns the reason err refused a capability for, and false when err is no such refusal — nil, a store failure, or a programming error such as ErrInvalidRequest, which a consumer maps as it maps any other.

func Reasons

func Reasons() []Reason

Reasons returns every reason this package defines, in a fixed order, so a consumer can prove its own mapping covers them all.

func (Reason) Permanent

func (r Reason) Permanent() bool

Permanent reports whether a refusal for r stands for as long as the capability does: the same request with the same capability will be refused again, whatever happens elsewhere, so a caller stops rather than retries.

A refusal that something else can lift is not permanent: a budget frees as holds lapse or charges are refunded, a tenant ceiling is raised by an operator, a taint flag is cleared, a not-yet-valid capability becomes valid, a proof is signed afresh, the verifier fetches its keys again, and an idempotency key can be added to the request.

type Record

type Record struct {
	Capability Capability
	// IssuedBy is the principal Insert was given.
	IssuedBy Principal
	// Revocation is the capability's own revocation entry; nil when it has
	// none. A capability stopped only through an ancestor has none — ask
	// IsRevoked whether it verifies.
	Revocation *Revocation
}

Record is a capability as the Store holds it.

type RefundReceipt added in v0.3.0

type RefundReceipt struct {
	// ChargeID is the charge refunded from.
	ChargeID uuid.UUID
	// Refunded is the amount this call returned; zero when nothing was
	// left to refund.
	Refunded Nanos
	// TenantID and UnitCode are the charge's.
	TenantID uuid.UUID
	UnitCode string
}

RefundReceipt is the result of Meter.Refund.

type RefundRequest

type RefundRequest struct {
	ChargeID uuid.UUID
	// Amount to refund; 0 = everything not yet refunded from this charge.
	Amount Nanos
}

RefundRequest is the input to Meter.Refund.

type RemoteJWKSConfig

type RemoteJWKSConfig struct {
	// URL of the issuer's JWKS document, e.g.
	// https://issuer.example/.well-known/jwks.json.
	URL string
	// Client performs the fetch. Default: a client with a 10s timeout.
	Client *http.Client
	// RefreshInterval is how long a fetched key set is used before it is
	// re-fetched, unless the response's Cache-Control says a shorter time
	// (RFC 9111): its max-age, less its Age, never below
	// MinRefreshInterval. Default 5m.
	RefreshInterval time.Duration
	// MinRefreshInterval rate-limits fetches. A token with an unknown kid
	// triggers a refetch (that is how a newly published key is picked up),
	// but at most once per this interval, so a stream of tokens with
	// made-up kids cannot turn the verifier into a request amplifier
	// against the issuer. Default 10s.
	MinRefreshInterval time.Duration
	// MaxStale is how long the last good key set keeps being served while
	// fetches fail. Past it the resolver fails closed with
	// ErrJWKSUnavailable. Default 1h — comfortably longer than the
	// capability TTLs, shorter than a key's useful life after withdrawal.
	MaxStale time.Duration
	// MaxDocumentBytes caps the response body. Default 1 MiB.
	MaxDocumentBytes int64
	// Now is the clock; tests override.
	Now func() time.Time
}

RemoteJWKSConfig configures a RemoteJWKSResolver. Only URL is required. The intervals must nest: MinRefreshInterval ≤ RefreshInterval ≤ MaxStale.

type RemoteJWKSResolver

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

RemoteJWKSResolver is a KeyResolver backed by an issuer's JWKS endpoint, for verifiers that run apart from the issuer. It caches the key set, revalidates with ETag, picks up a rotated-in key on first sight of its kid, and keeps serving the last good set through a bounded outage.

func NewRemoteJWKSResolver

func NewRemoteJWKSResolver(cfg RemoteJWKSConfig) (*RemoteJWKSResolver, error)

NewRemoteJWKSResolver validates the config. It does not fetch: the first PublicKey call does, so constructing a verifier never blocks on the network.

func (*RemoteJWKSResolver) PublicKey

func (r *RemoteJWKSResolver) PublicKey(ctx context.Context, kid string) (ed25519.PublicKey, error)

PublicKey implements KeyResolver.

type ReplayCache

type ReplayCache interface {
	// Seen records jti until expires and reports whether it was already
	// recorded. An implementation that cannot tell — its store is
	// unreachable — reports true: an unanswerable check refuses the proof.
	Seen(ctx context.Context, jti string, expires time.Time) bool
}

ReplayCache remembers proof ids for as long as a proof could be accepted.

type Reservation

type Reservation struct {
	ID           uuid.UUID
	CapabilityID uuid.UUID
	TenantID     uuid.UUID
	// Amount is held in UnitCode.
	Amount   Nanos
	UnitCode string
	// Op and Actor are what Settle will stamp on the charge.
	Op    string
	Actor string
	// Copies are the Biscuit copies the hold is also on, innermost first,
	// with the budget each is held against; a hold counts no requests, so
	// their MaxRequests is zero.
	Copies    []CopyCeiling
	ExpiresAt time.Time
}

Reservation is a committed hold, as Reserve made it and as GetReservation and ListReservations read it back.

type ReserveRequest

type ReserveRequest struct {
	CapabilityID uuid.UUID
	// TenantID is the capability's tenant. Required.
	TenantID uuid.UUID
	// Amount to hold, in UnitCode. Must be finite and ≥ 0.
	Amount Nanos
	// MaxBudget is the capability's own ceiling, from the verified token.
	MaxBudget Nanos
	UnitCode  string
	// TTL is how long the hold lasts if never settled or released.
	// 0 = DefaultReservationTTL.
	TTL time.Duration
	// Op and Actor are carried onto the charge Settle records.
	Op    string
	Actor string
	// Copies are the presented Biscuit copy's own limits (Capability.Copies).
	// The reservation keeps them, and Settle checks and charges them.
	Copies []CopyCeiling
}

ReserveRequest is the input to Meter.Reserve.

type Revocation

type Revocation struct {
	RevokedAt time.Time
	Reason    string
	Actor     string
	// Cascade reports that the entry was written by a cascading revoke —
	// of this capability or of an ancestor.
	Cascade bool
}

Revocation is one entry of the revocation list, as Revoke wrote it.

type RevocationLookup

type RevocationLookup interface {
	IsRevoked(ctx context.Context, id uuid.UUID) (bool, error)
}

RevocationLookup is the seam the cache wraps. A persistent store implements it; tests substitute a stub. Like Store.IsRevoked, it answers for the capability's whole delegation chain.

type RevokeBiscuitRequest

type RevokeBiscuitRequest struct {
	// CapabilityID is the capability the Biscuit seals.
	CapabilityID uuid.UUID
	// RevocationID is the id to list: BiscuitCopy.RevocationID.
	RevocationID []byte
	// Reason and Actor are stored for audit, as on RevokeRequest.
	Reason string
	Actor  string
}

RevokeBiscuitRequest is the input shape for BiscuitRevocationStore.RevokeBiscuit.

type RevokeRequest

type RevokeRequest struct {
	ID uuid.UUID
	// Reason is an operator-supplied label ("compromise", "rotation",
	// "policy-change"). Stored on the revocation row for audit.
	Reason string
	// Actor identifies who triggered the revocation (a user UUID or
	// an automation principal). Stored on the row.
	Actor string
	// CascadeChildren records a revocation entry for every descendant as
	// well. Descendants stop verifying regardless; see Store.Revoke.
	CascadeChildren bool
}

RevokeRequest is the input shape for Store.Revoke.

type SetTenantBudgetRequest

type SetTenantBudgetRequest struct {
	TenantID        uuid.UUID
	MaxBudgetAmount Nanos
	// UnitCode pins the currency. Empty = keep existing or default
	// to DefaultUnitCode on first insert.
	UnitCode string
	// PeriodEnd, if non-nil, pins a closing time for the accounting
	// window. Nil = open-ended (typical for "just set the cap, I'll
	// roll later").
	PeriodEnd *time.Time
	// ResetSpend=true zeroes the spend and rolls period_start to
	// now. false leaves the counter alone — the cap changes mid-
	// window.
	ResetSpend bool
	// ExpectedVersion is the OCC guard, checked in the same statement as the
	// write. 0 asserts no ceiling has been set yet (the row is missing, or
	// holds only counted spend) and is itself a conflict if one has. Without this two operators editing the same cap overwrote each
	// other and neither was told.
	ExpectedVersion int64
}

SetTenantBudgetRequest is the input for UsageStore.SetTenantBudget.

type SettleRequest

type SettleRequest struct {
	ReservationID uuid.UUID
	// Amount is the actual cost to charge. Must be finite and ≥ 0.
	Amount Nanos
	// MaxBudget is the capability's own ceiling, from the verified token.
	MaxBudget Nanos
	// Overrun is what happens when Amount would cross a ceiling.
	Overrun OverrunPolicy
}

SettleRequest is the input to Meter.Settle.

type Signer

type Signer interface {
	// KeyID is the JWKS key ID embedded in tokens this signer mints.
	// Verifiers index their key set by it, and rotation works by
	// running two signers in parallel until clients have caught up.
	KeyID() string

	// Sign emits a compact JWT for the supplied capability. The cap's
	// IssuedAt / NotBefore / ExpiresAt must already be populated; Sign
	// does not normalise times. ctx bounds the signing, which for a key
	// held in a KMS or an HSM is a call over the network.
	Sign(ctx context.Context, c Capability) (string, error)
}

Signer mints capability tokens with an Ed25519 key; verifiers pick up the public keys (statically or from a JWKS endpoint) and validate locally — no RPC per request. Implement it over a KMS to keep the private key out of process memory.

Signer is intentionally narrow: callers populate a Capability struct and ask Sign to encode + sign. The compact JWT format is the wire representation; Decode reads one back without the verifier path, for debugging tools, and VerifySignature checks its signature alone.

func NewEd25519Signer

func NewEd25519Signer(keyID string, priv ed25519.PrivateKey) (Signer, error)

NewEd25519Signer constructs a signer from a key ID and Ed25519 private key. Caller is responsible for key generation / KMS interaction — this package is pure crypto + encoding.

type SigningKey added in v0.3.0

type SigningKey struct {
	KeyID   string
	Public  ed25519.PublicKey
	Private ed25519.PrivateKey
	// Generated reports that the key was made fresh rather than read from a
	// file: it vanishes with the process, and every token signed with it
	// stops verifying once it does.
	Generated bool
}

SigningKey is an Ed25519 key and the kid it is published under.

func LoadOrGenerateKey added in v0.3.0

func LoadOrGenerateKey(path, kid string) (SigningKey, error)

LoadOrGenerateKey reads the key at path (LoadPrivateKey) or, when path is empty, generates an ephemeral one. kid names it; empty derives one (DeriveKeyID). A path that cannot be read is an error, never a reason to fall back to a fresh key. Log a generated key loudly: an operator who forgot to mount the file finds out on the first restart otherwise.

type StandardVerifier

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

StandardVerifier is the production verifier — signature first, then claims, then issuer / tenant / time / audience / revocation gates.

Verification is independent of the persistence layer apart from the revocation lookup, so the verifier scales horizontally without contention on the capability records.

func NewStandardVerifier

func NewStandardVerifier(cfg VerifierConfig) (*StandardVerifier, error)

NewStandardVerifier validates wiring and builds the verifier.

func (*StandardVerifier) Authorize added in v0.3.0

Authorize implements Verifier.

Example
package main

import (
	"context"
	"crypto/ed25519"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

	"github.com/oleg-tkachuk/limes"
	"github.com/oleg-tkachuk/limes/memstore"
)

// The issuer's name, and the audience every example's tokens are minted for.
const (
	exampleIssuerName = "example-issuer"
	exampleAudience   = "my-service"
)

// exampleStack is an issuer and a verifier over one in-memory store — the
// setup ExampleIssuer_Issue shows in full.
type exampleStack struct {
	issuer   *limes.Issuer
	verifier *limes.StandardVerifier
	records  *memstore.Store[struct{}]
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	kid, pub, priv, err := limes.GenerateEd25519Keypair()
	if err != nil {
		log.Fatal(err)
	}
	records := memstore.New[struct{}]()
	signer, err := limes.NewEd25519Signer(kid, priv)
	if err != nil {
		log.Fatal(err)
	}
	issuer, err := limes.NewIssuer(limes.IssuerConfig{Signer: signer, Store: records, IssuerName: exampleIssuerName})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewStandardVerifier(limes.VerifierConfig{
		Keys:               limes.NewStaticKeyResolver(map[string]ed25519.PublicKey{kid: pub}),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               &limes.DPoPVerifier{Replay: limes.NewMemoryReplayCache(0)},
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: memstore.NewUsage[struct{}](records), tenantID: uuid.New(),
	}
}

// issue mints a capability for an agent: get and share under corpus/public/,
// 25.00 USD and 500 requests, for 15 minutes.
func (s exampleStack) issue() (limes.Capability, string) {
	c, token, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalAgent, TenantID: s.tenantID, Subject: "research-orchestrator",
		},
		Audience: []string{exampleAudience},
		TTL:      15 * time.Minute,
		Caveats: limes.Caveats{
			Ops:              []limes.Op{limes.OpGet, limes.OpShare},
			ResourcePrefixes: []string{"corpus/public/"},
			MaxRequests:      500,
			MaxBudgetAmount:  limes.MustParseAmount("25.00"),
			UnitCode:         "USD",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	_, token := s.issue()

	// One call: the token, then every caveat that needs no state.
	authorize := func(op limes.Op, resource string) error {
		_, err := s.verifier.Authorize(ctx, limes.AuthorizeRequest{
			VerifyRequest: limes.VerifyRequest{Token: token, Audience: exampleAudience},
			CheckRequest:  limes.CheckRequest{Op: op, Resource: resource},
		})
		return err
	}
	fmt.Println(authorize(limes.OpGet, "corpus/public/report.pdf"))
	fmt.Println(errors.Is(authorize(limes.OpDelete, "corpus/public/report.pdf"), limes.ErrOpNotAllowed))
	fmt.Println(errors.Is(authorize(limes.OpGet, "corpus/private/x"), limes.ErrResourceNotAllowed))
}
Output:
<nil>
true
true

func (*StandardVerifier) BiscuitCopy

func (v *StandardVerifier) BiscuitCopy(ctx context.Context, token string) (BiscuitCopy, error)

BiscuitCopy reads which copy token is, after checking it as Verify does — the sealed token's signature, issuer and tenant, then the Biscuit's signature chain — so that a revocation is only ever recorded against a capability the token genuinely carries. Expiry, audience and revocation are not checked: a copy can be revoked whatever its state. Nor is MeterCopies: a copy carrying limits is named, and can be revoked, on any verifier.

func (*StandardVerifier) DPoPNonce added in v0.3.0

func (v *StandardVerifier) DPoPNonce(ctx context.Context) (string, error)

DPoPNonce is the nonce to send in the DPoP-Nonce header — with a response refused by ErrDPoPNonceRequired, and with any other for the client's next proof. Empty when the verifier issues none.

func (*StandardVerifier) ErrorResponse added in v0.3.0

func (v *StandardVerifier) ErrorResponse(ctx context.Context, err error, scheme AuthScheme) ErrorResponse

ErrorResponse is how to answer a request refused with err, presented under scheme. A refusal about the token is challenged under the scheme the client used (Bearer when it named none); one about the proof, under DPoP. A verifier with DPoP configured also advertises its algorithms in a DPoP challenge, and hands out a fresh nonce with use_dpop_nonce.

func (*StandardVerifier) Verify

Verify implements Verifier. Returns the typed Capability on success. Errors return one of the package's typed sentinels so the caller can branch on the failure mode.

Order matters: nothing in the claims is read until the signature over them has verified, so a forged token costs a header decode and one signature check, and no claim-parsing path is reachable by an attacker. The checks that need no I/O come before the revocation lookup.

type StaticKeyResolver

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

StaticKeyResolver is the in-memory implementation used in tests and single-node deploys. Maps kid → public key; concurrency-safe via a sync.RWMutex so rotation can swap a kid without restarting callers.

func NewStaticKeyResolver

func NewStaticKeyResolver(keys map[string]ed25519.PublicKey) *StaticKeyResolver

NewStaticKeyResolver builds a resolver from a kid → public key map. The map is copied to defend against caller mutation.

func (*StaticKeyResolver) Keys

func (r *StaticKeyResolver) Keys() map[string]ed25519.PublicKey

Keys returns a snapshot of the current key set, e.g. for MarshalJWKS.

func (*StaticKeyResolver) PublicKey

func (r *StaticKeyResolver) PublicKey(_ context.Context, kid string) (ed25519.PublicKey, error)

PublicKey implements KeyResolver.

func (*StaticKeyResolver) RemoveKey

func (r *StaticKeyResolver) RemoveKey(kid string)

RemoveKey withdraws a kid. Rotation step 3: call it only once the longest outstanding TTL signed under that key has elapsed.

func (*StaticKeyResolver) SetKey

func (r *StaticKeyResolver) SetKey(kid string, key ed25519.PublicKey)

SetKey adds or replaces a kid → key mapping. Rotation step 1: publish the new key alongside the old.

type Store

type Store interface {
	// Insert records a capability at issuance. The persisted row holds
	// the full claim set so admin tooling can show a capability without
	// parsing the JWT, and so audit can correlate without
	// keeping every issued token.
	//
	// issuedBy is the principal that ASKED for the capability, which is not
	// c.Subject (the principal it authorises) and not c.Issuer (the service
	// that minted it). A delegated capability is requested by its parent's
	// holder; a root one by an operator. Passed as an argument rather than
	// carried on Capability because it is issuance metadata, not a claim —
	// putting it in the struct would change the token's frozen wire format.
	//
	// An id already on record returns ErrAlreadyExists and changes nothing.
	// A store that knows tenants returns ErrUnknownTenant or
	// ErrTenantDeleted for a tenant it cannot mint for.
	Insert(ctx context.Context, c Capability, issuedBy Principal) error

	// Get fetches a capability record by ID. Returns ErrNotFound when
	// no row exists; callers normally treat that as ErrInvalidSignature
	// (token forgery) rather than a missing entity.
	//
	// ErrNotFound is defined in THIS package (below), not in any store
	// implementation — a third party writing its own Store must have a
	// sentinel to return without importing someone else's persistence.
	Get(ctx context.Context, id uuid.UUID) (Capability, error)

	// GetRecord reads back everything Insert and Revoke wrote about a
	// capability: the capability, who issued it, and its own revocation
	// entry if it has one. ErrNotFound as Get. Get stays the read on the
	// verification path; this one is for audit and admin tooling.
	GetRecord(ctx context.Context, id uuid.UUID) (Record, error)

	// IsRevoked reports whether the capability, or any ancestor of it
	// that is still on record, is in the revocation list. Answering for
	// the chain is what makes revoking an orchestrator stop the workers it
	// spawned. Verifiers gate every request on it, so implementations
	// should make the walk a single round trip (a recursive query) and
	// bound its depth. An id not on record is not revoked.
	IsRevoked(ctx context.Context, id uuid.UUID) (bool, error)

	// Revoke adds an entry to the revocation list. Idempotent: a
	// duplicate revoke is a no-op. An id not on record — or not visible
	// to the caller — returns ErrNotFound and writes nothing, so a caller
	// is never told "revoked" about a capability nothing stopped.
	//
	// Every descendant stops verifying either way, because IsRevoked
	// walks the chain. CascadeChildren=true additionally writes a
	// revocation entry for each descendant, so the audit trail names
	// every capability that was stopped, not only the one an operator
	// picked.
	Revoke(ctx context.Context, req RevokeRequest) error

	// PurgeExpired drops the revocation entries — of capabilities and of
	// Biscuit copies — of every capability expired for at least
	// expiredFor, and returns how many it dropped. The capability records
	// stay: an expired token fails verification on its expiry alone, so
	// its revocation entry no longer stops anything. Run by the
	// housekeeping worker so the revocation lists stay bounded.
	PurgeExpired(ctx context.Context, expiredFor time.Duration) (int64, error)

	// ListByPrincipal lists the capabilities issued to one principal, in
	// ascending id order, a page at a time: it returns at most Limit of
	// them and a cursor that is empty on the last page and otherwise
	// fetches the next one. The cursor is opaque.
	ListByPrincipal(ctx context.Context, req ListByPrincipalRequest) ([]Capability, string, error)
}

Store persists capability records and revocation entries. Consumers supply the implementation; memstore ships an in-memory one and the reference deployment supplies a relational one.

Records are append-mostly: capabilities aren't mutated after issuance except via Revoke. Revocations are checked by the verifier on every request; put a CachedRevocationChecker in front to keep the per-call cost flat.

type TenantBudget

type TenantBudget struct {
	TenantID        uuid.UUID
	MaxBudgetAmount Nanos
	SpentAmount     Nanos
	// ReservedAmount is held by the tenant's open reservations.
	ReservedAmount Nanos
	UnitCode       string
	PeriodStart    time.Time
	PeriodEnd      *time.Time
	UpdatedAt      time.Time
	// ResourceVersion is the OCC token a caller passes back to Set. Zero
	// means no ceiling has ever been set — the row, if there is one, holds
	// only what Charge and Reserve counted.
	ResourceVersion int64
}

TenantBudget is the snapshot view of the tenant aggregate cap. Amounts are Nanos, exact; a store keeps them exactly too (an integer or decimal column), so that sums do not drift.

type TenantBudgetCursor

type TenantBudgetCursor struct {
	Utilisation string
	TenantID    uuid.UUID
}

TenantBudgetCursor is a place in ListTenantBudgets' order: the last row's utilisation, in the decimal text its store computed it in, and tenant id.

func DecodeTenantBudgetCursor

func DecodeTenantBudgetCursor(s string) (TenantBudgetCursor, error)

DecodeTenantBudgetCursor reads a cursor Encode wrote. Anything else is an ErrInvalidRequest.

func (TenantBudgetCursor) Encode

func (c TenantBudgetCursor) Encode() string

Encode is the cursor as ListTenantBudgets hands it out: opaque, URL-safe.

type TenantBudgetLister added in v0.2.0

type TenantBudgetLister interface {
	ListTenantBudgets(ctx context.Context, req ListTenantBudgetsRequest) ([]TenantBudgetSummary, string, error)
}

TenantBudgetLister lists tenant budgets a page at a time. Every TenantBudgets is one.

type TenantBudgetSummary

type TenantBudgetSummary struct {
	TenantID    uuid.UUID
	Slug        string
	DisplayName string
	Budget      TenantBudget
	// UtilisationPct = spent / max × 100, clamped to [0, 100]. 0
	// when MaxBudgetAmount == 0 (unlimited/metering-only).
	UtilisationPct float64
}

TenantBudgetSummary is a TenantBudget with the consumer's display fields (empty when the consumer keeps none). Returned by ListTenantBudgets.

type TenantBudgets

type TenantBudgets interface {
	// GetTenantBudget returns the tenant-aggregate snapshot. Returns
	// ErrTenantBudgetNotFound when no row exists (tenant has no cap
	// configured AND has never been charged).
	//
	// The aggregate counts from the tenant's first charge or reservation,
	// whether or not a ceiling has been set: Charge and Reserve create the
	// row, with no ceiling and ResourceVersion 0, when it is missing. A
	// ceiling set afterwards is measured against that spend and those holds.
	GetTenantBudget(ctx context.Context, tenantID uuid.UUID) (TenantBudget, error)

	// SetTenantBudget upserts the tenant cap. ResetSpend=true rolls
	// the accounting period and zeroes the spend — operators call
	// this on each billing close. ResetSpend=false adjusts the cap
	// mid-cycle without affecting accumulated spend. A ceiling outside
	// 0..MaxNanos returns ErrInvalidAmount and changes nothing. A UnitCode
	// other than the row's returns ErrUnitCodeMismatch while anything is
	// held in the old unit, or spent in it and not reset.
	SetTenantBudget(ctx context.Context, req SetTenantBudgetRequest) (TenantBudget, error)

	// ListTenantBudgets returns tenant budget rows, with the consumer's
	// display fields where it has them. Filters:
	//   - excludeInactive: skip tenants the consumer considers inactive.
	//   - thresholdPct > 0: include only rows where
	//     spent / max * 100 >= thresholdPct (and max > 0).
	//   - unlimitedOnly: include only rows where max == 0.
	//   - limit: the page size, as PageLimit reads it.
	//
	// Rows come most at risk first: by spent / max × 100 descending —
	// unclamped, so of two tenants past their ceilings the further one
	// leads — then by tenant id. A page ends with a cursor that fetches
	// the next one, empty on the last page. Spend moves between pages, so
	// a tenant may show on two of them, or on none; a cursor is a place in
	// the order, not a snapshot.
	ListTenantBudgets(ctx context.Context, req ListTenantBudgetsRequest) ([]TenantBudgetSummary, string, error)
}

TenantBudgets administers the tenant-aggregate ceilings Charge enforces.

type Usage

type Usage struct {
	CapabilityID uuid.UUID
	RequestCount int64
	SpentAmount  Nanos
	// ReservedAmount is held by open reservations (its subtree's, for a
	// capability with delegated children).
	ReservedAmount Nanos
	UnitCode       string
}

Usage is the snapshot view of a capability's runtime counters.

type UsageHousekeeping

type UsageHousekeeping interface {
	// Delete drops the row. Called when the capability is reaped. It
	// releases the capability's open reservations first, returning their
	// holds to its ancestors and its tenant: nothing could settle or release
	// them afterwards.
	Delete(ctx context.Context, capID uuid.UUID) error

	// PurgeOrphans deletes usage rows whose capability record no longer
	// exists, releasing their reservations as Delete does. Implementations may bound the work per call; the caller
	// loops until it returns 0.
	PurgeOrphans(ctx context.Context) (int64, error)

	// ReleaseExpired releases every reservation past its expiry and
	// returns how many it released. Run on a timer: until it runs, an
	// expired hold keeps counting against the ceilings, which errs towards
	// refusing spend, never towards allowing too much.
	ReleaseExpired(ctx context.Context) (int64, error)
}

UsageHousekeeping reclaims usage rows of capabilities that are gone.

type UsageStore

type UsageStore[TX any] interface {
	Meter[TX]
	TenantBudgets
	UsageHousekeeping
}

UsageStore is everything a consumer implements for runtime accounting. It is the union of three narrower contracts so that code which only meters (an interceptor, a handler) can depend on Meter alone, and code which only administers tenant ceilings can depend on TenantBudgets alone.

func WithMetering

func WithMetering[TX any](inner UsageStore[TX], opts ...MeteringOption) (UsageStore[TX], error)

WithMetering wraps a UsageStore in the metering decorator. Its instruments are created on the global MeterProvider unless WithMeterProvider names another, and carry no tenant unless WithTenantAttribute asks for it.

The result implements CopyUsageReader exactly when inner does, so wrapping a store never hides what it can read.

type Verifier

type Verifier interface {
	// Verify checks the token itself: its signature, issuer and tenant, its
	// validity window, its audience, the proof of possession a key-bound
	// capability needs, and revocation. It returns the capability the token
	// grants.
	Verify(ctx context.Context, req VerifyRequest) (Capability, error)

	// Authorize is Verify followed by every caveat that can be checked
	// without state: the operation, the resource, the idempotency key, the
	// tainted read (Caveats.Check), and the client's address
	// (Caveats.CheckSource). The request and budget limits are stateful:
	// the Meter enforces them, through Bump and Charge, inside the
	// consumer's own transaction.
	Authorize(ctx context.Context, req AuthorizeRequest) (Capability, error)
}

Verifier validates tokens. A deployment typically resolves keys through a cached JWKS (RemoteJWKSResolver) and checks revocation through a short-TTL cache (CachedRevocationChecker), so revocation propagates quickly without hammering the store.

type VerifierConfig

type VerifierConfig struct {
	// Keys resolves JWKS kids to Ed25519 public keys.
	Keys KeyResolver

	// Revocations answers whether a capability — or any ancestor in its
	// delegation chain — is revoked. Wrap the store in a
	// CachedRevocationChecker to keep the per-call cost flat.
	Revocations RevocationLookup

	// TrustedIssuers is the set of issuer values the verifier accepts.
	// Tokens whose `iss` claim is not in the set are rejected with
	// ErrInvalidSignature.
	TrustedIssuers []string

	// KeyIssuers, when non-nil, binds each kid to the one issuer allowed
	// to sign with it: a token whose kid is absent from the map, or whose
	// `iss` differs from the kid's binding, is rejected. Without it any
	// trusted key may sign for any trusted issuer, which is fine for one
	// issuer and wrong for several that do not share a trust boundary.
	KeyIssuers map[string]string

	// Now is the time function the verifier uses for nbf/exp. Tests
	// override it; production passes time.Now.
	Now func() time.Time

	// Leeway widens the nbf / exp window to absorb clock skew between
	// issuer and verifier. Default 30s when zero.
	Leeway time.Duration

	// MaxTokenBytes rejects longer tokens before decoding them. Default
	// DefaultMaxTokenBytes when zero.
	MaxTokenBytes int

	// AcceptBiscuit accepts the Biscuit form (Issuer.Biscuit, Attenuate)
	// beside the JWT. Off, a Biscuit is refused like any malformed token.
	AcceptBiscuit bool

	// MeterCopies admits Biscuits whose attenuation blocks set a copy's own
	// request or budget limits (Attenuation.MaxRequests, MaxBudgetMicros).
	// Set it only when the Meter that enforces caveats counts
	// BumpRequest.Copies and the like; off, such a token is refused with
	// ErrCopyCountersNotMetered rather than accepted with its limits unkept.
	MeterCopies bool

	// BiscuitRevocations answers whether a copy of a Biscuit is revoked on
	// its own. Required when AcceptBiscuit is set; wrap the store in a
	// CachedBiscuitRevocationChecker.
	BiscuitRevocations BiscuitRevocationLookup

	// DPoP checks the proof of possession a key-bound capability (one with
	// a ConfirmationJKT) must come with. Without it the verifier refuses
	// every key-bound capability with ErrDPoPRequired, rather than accept
	// one as a bearer token.
	DPoP *DPoPVerifier
}

VerifierConfig wires the verifier. Keys, Revocations and TrustedIssuers are required.

type VerifyRequest added in v0.3.0

type VerifyRequest struct {
	// Token is the capability token, a JWT or a Biscuit.
	Token string
	// Audience is the service the call is hitting: the token's audience
	// must include it.
	Audience string
	// DPoP is the request's proof of possession, needed when the
	// capability is key-bound and ignored when it is not.
	DPoP DPoPProof
	// Scheme is the Authorization scheme the token came under
	// (ParseAuthorization). Set, it must match the binding: DPoP for a
	// key-bound capability, Bearer for any other.
	Scheme AuthScheme
}

VerifyRequest is a token as it arrived, and what it arrived for.

Directories

Path Synopsis
biscuit module
Command example is the quickstart walkthrough as a runnable program: issue → verify → delegate → revoke, against the in-memory reference store.
Command example is the quickstart walkthrough as a runnable program: issue → verify → delegate → revoke, against the in-memory reference store.
exchange module
Package memstore is an in-memory reference implementation of the limes storage contracts.
Package memstore is an in-memory reference implementation of the limes storage contracts.
Package metertest checks a limes.Meter against the module's contract, the way testing/fstest checks an fs.FS.
Package metertest checks a limes.Meter against the module's contract, the way testing/fstest checks an fs.FS.
otelmetric module
pgstore module
redisreplay module
Package storetest checks a limes.Store — and, when it is one, a limes.BiscuitRevocationStore — against the module's contract, the way testing/fstest checks an fs.FS.
Package storetest checks a limes.Store — and, when it is one, a limes.BiscuitRevocationStore — against the module's contract, the way testing/fstest checks an fs.FS.

Jump to

Keyboard shortcuts

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