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

limes

package module
v0.77.3 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 42 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 — through the issuer, or offline as a Biscuit block with no call at all — 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/>Authorize · Bump · 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

Setup errors are dropped below for brevity; each constructor returns one.

ctx := context.Background()
tenantID, runID := uuid.New(), uuid.New() // github.com/google/uuid
key, _ := limes.GenerateKey()
kid, pub, priv := key.KeyID, key.Public(), key.Private

records, _ := memstore.NewStore(memstore.StoreConfig{}) // your Store
usage, _ := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})

signer, _ := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
issuer, _ := limes.NewIssuer(limes.IssuerConfig{
    Signer: signer, Store: records, IssuerName: "my-issuer",
})
keys, _ := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
verifier, _ := limes.NewVerifier(limes.VerifierConfig{
    Keys:           keys,
    Revocations:    records,
    TrustedIssuers: []string{"my-issuer"},
})

issued, _ := issuer.Issue(ctx, limes.IssueRequest{
    IssuedBy: limes.Principal{Subject: "operator@example.com"}, // who asked
    Subject: limes.Principal{
        Type: limes.PrincipalTypeAgent, 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/"},
        Requests:         limes.RequestCeiling{Max: 500},
        Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
    },
})

// On every call: verify locally and enforce the caveats in one step, then meter.
granted, err := verifier.Authorize(ctx, limes.AuthorizeRequest{
    Verify: limes.VerifyRequest{Token: issued.Token, Audience: "my-service"},
    Check:  limes.CheckRequest{Op: limes.OpGet, Resource: "corpus/public/report.pdf"},
})
if err != nil {
    return err // a refusal, e.g. limes.ErrResourceNotAllowed outside corpus/public/
}
subject := granted.MeterSubject()
if _, err := usage.Bump(ctx, limes.BumpRequest{Subject: subject}); err != nil {
    return err // limes.ErrRequestLimitExceeded past the 500th request
}
if _, err := usage.Charge(ctx, limes.ChargeRequest{
    Subject: subject, Amount: limes.MustParseAmount("0.35"),
}, nil); err != nil {
    return err // limes.ErrBudgetExceeded past 25.00, with nothing counted
}

IssueRequest requires Subject with its TenantID, IssuedBy (a Subject at least), Audience and Caveats; TTL defaults to fifteen minutes. issued.Token is what the agent carries; issued.Capability is the record you keep for audit. verifier.ErrorResponse(ctx, err, limes.AuthSchemeBearer) turns a refusal into the HTTP status, WWW-Authenticate challenge and problem body to answer with. go run ./quickstart in examples/ runs issue, authorize, delegate, meter and revoke end to end; the other examples there take each step on its own, pgstore included, logging what happens.

What it does

Feature What it does
Verify Local Ed25519 JWT verification (alg: Ed25519, RFC 9864) against a resolved key — static, or an issuer's JWKS fetched by jwks.Remote, which revalidates by ETag and honours Cache-Control. Claims are read strictly: no unknown or duplicate member, integers within I-JSON's range. Rejections match sentinel errors (errors.Is) in families — ErrUnauthenticated over ErrInvalidToken and ErrProofOfPossession — and kinds that must not tell an attacker which check failed share ErrInvalidToken.
Enforce Authorize verifies and checks every stateless caveat in one call: allowed ops, resource prefixes matched at / boundaries (a . or .. segment never matches), idempotency keys for writes, tainted reads, source networks. Request and budget limits are the Meter's.
Delegate Issuer.Delegate issues a child that may only be narrower — operations, resources, limits, audience, and a validity window that starts no earlier and ends no later than the parent's; anything wider is ErrDelegationTooWide. The parent must allow OpShare, or the delegation is ErrOpNotAllowed.
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, presented under the DPoP scheme; server nonces (dpop.HMACNonces) work across replicas without shared state.
Meter Requests count against the capability and every ancestor; spend against those 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 an ISO 4217 unit. Instrument adds OpenTelemetry instruments.
Revoke One call stops a capability and everything delegated from it; verifiers learn of it within a cache TTL, or at once on Clear, which pgstore.RevocationWatcher calls on every revocation.
Report LedgerReader.SumCharges sums a tenant's charges over a period by unit, whole or by UTC hour, day, ISO week or month, and by operation, actor or capability, the largest first. RecordCensus.CountRecords counts capability records — active, expired, revoked, delegated, expiring soon — for one tenant or all, by tenant and principal type.
Answer refusals ErrorResponse maps a refusal to its status, an RFC 6750 / RFC 9449 WWW-Authenticate challenge and an RFC 9457 problem body, without the error's own text.

You implement Store and UsageStore[TX] — or use memstore, or pgstore over PostgreSQL — and a KeyResolver, which jwks.Static and jwks.Remote already are, and check your implementation from its own tests with storetest, metertest and jwkstest (dpoptest for a DPoP replay cache or nonce source). UsageStore is generic in your transaction type, so a charge can commit together with, say, an outbox write; with no transactions, use UsageStore[struct{}]. Optional contracts serve one feature each: BiscuitRevocationStore and CopyUsageReader for Biscuit copies, LedgerReader for billing reads, RecordCensus for a census of records, and TenantRecordEraser and TenantUsageEraser to remove a tenant.

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.
  • The root module's tests need nothing: no database, no network, no container. pgstore's run against PostgreSQL in a container, and redisreplay's against miniredis and, in CI, a real Redis.

Versioning

Releases are vX.Y.Z tags, cut from Conventional Commits on main when CI passes: feat is a minor; fix, perf and security 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, which list breaking changes first, before bumping.

redisreplay and pgstore are modules of their own, each with a release line of its own — redisreplay/vX.Y.Z, pgstore/vX.Y.Z — cut by the same rules from the commits that touch it, plus one: a module's own build commit, such as build(pgstore): require limes vX.Y.Z, is a patch of it. A commit releases, by its type, each module whose files it changes; one scoped pgstore or redisreplay never releases limes, breaking or not.

The token wire format — claims, typ: limes-cap-v4+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 a version it does not accept (VerifierConfig.AcceptTokenTypes) outright, rather than read its claims under the wrong schema. Every 1.x verifier accepts limes-cap-v4+jwt, so a later version ships in a minor release: verifiers accept it first, issuers mint it after. Tokens are signed with Ed25519 throughout 1.x; another algorithm would be another token version, never a runtime choice.

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
docs/migration.md every breaking change from v0.3.0 on, of limes and of pgstore, release by release, as what it was and what it is now
docs/api-review-v1.md the review of the API before 1.0: each finding, the decision taken and whether it is done
CONTRIBUTING.md building, testing and how a change lands
SECURITY.md reporting a vulnerability

Layout

Path Holds
/ the limes package: issuer, verifier, caveats, Biscuit, metering, contracts
jwks/ key sets (RFC 7517): the JWKS document and handler, remote and static key resolvers
dpop/ proof of possession (RFC 9449): signing and checking proofs, replay cache, server nonces
inspect/ reading a token without verifying it, for tooling
memstore/ the in-memory reference implementation of every storage contract
pgstore/ the reference PostgreSQL store, a module of its own: Store, BiscuitRevocationStore, UsageStore[pgx.Tx], CopyUsageReader, a Ledger, a Census and both tenant erasers under row-level security per tenant, with EraseTenant and a RevocationWatcher; held to storetest and metertest; released as pgstore/vX.Y.Z
redisreplay/ a dpop.ReplayCache over Redis, a module of its own, for verifiers behind several replicas; released as redisreplay/vX.Y.Z
internal/ the module's own shared packages, not importable from outside it: JOSE pieces (jose), HTTP header names (httpx), RFC 3986 URI normalisation (rfc3986), access to unexported root internals for sibling packages (link), and field coverage and per-check deadlines for the conformance suites (fields, watchdog)
storetest/, metertest/, dpop/dpoptest, jwks/jwkstest conformance suites to run from your own implementation's tests
examples/ runnable programs: the five-minute walkthrough (quickstart), and one topic each, logging every step to stdout — issuing and verifying, delegation, metering, and pgstore over PostgreSQL; a module of its own, never released
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, golangci-lint: tools/golangci-lint/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, together with everything it delegated.
  • Sub-capabilities without an operator: an orchestrator narrows its own authority for each sub-agent it spawns, through the Issuer with no admin API in the path, or offline as a Biscuit block with no call at all.

Wire format is JWT compact serialization with Ed25519 signatures (`alg: Ed25519`, RFC 9864), or a Biscuit v3 sealing such a token; docs/standards.md lists every standard followed. Verification is local: the signature is checked against a resolved public key, and the store reads, whether the capability is revoked and, for a Biscuit, whether its copy is, go through short-lived caches (CachedRevocationChecker, CachedBiscuitRevocationChecker), so consumers scale horizontally without contending on a shared store.

Packages

Two standards the verifier relies on are packages of their own, usable without the rest: github.com/oleg-tkachuk/limes/jwks for key sets (RFC 7517) and github.com/oleg-tkachuk/limes/dpop for proof of possession (RFC 9449). github.com/oleg-tkachuk/limes/inspect reads a token without verifying it, for tooling. memstore, storetest and metertest serve stores; dpop/dpoptest and jwks/jwkstest check a replay cache, a nonce source and a key resolver. Two modules of their own ship stores with a driver the root module does not resolve: github.com/oleg-tkachuk/limes/pgstore, every store contract over PostgreSQL, and github.com/oleg-tkachuk/limes/redisreplay, a replay cache over Redis.

What you implement

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

  • Store — capability records and revocations; an Issuer needs only its IssuanceStore half. IsRevoked answers for the capability's whole delegation chain, at most MaxDelegationDepth ancestors deep. No transactions required; an in-memory map is a valid implementation. A store that scopes calls to a tenant refuses one whose request names another with ErrScopeMismatch.
  • UsageStore — request and spend counters and the charges ledger; the union of Meter, MeterReader, 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. jwks.Static ships as a working implementation and supports rotation via SetKey / RemoveKey; jwks.Remote fetches an issuer's JWKS for verifiers that run apart from it.

Eight more are needed only with the feature they serve: BiscuitRevocationStore to accept Biscuit tokens, CopyUsageReader to show a Biscuit copy's own counters, LedgerReader to sum a tenant's charges for billing and reports, RecordCensus to count capability records by disposition across tenants, TenantRecordEraser and TenantUsageEraser to remove a tenant, dpop.ReplayCache to share DPoP replay state across replicas (dpop.MemoryReplayCache ships for one process), and dpop.Nonces to require server nonces (dpop.HMACNonces ships, and works across replicas holding the same keys).

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 (BudgetCeiling.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.GetRecord a capability, MeterReader.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 and the charge's receipt (Charged), so a side effect such as an outbox write commits atomically with the spend and names the charge. 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 matchable sentinels, so a consumer can map each to its own transport status, or let Verifier.ErrorResponse map it to HTTP. 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 needs OpShare on the parent and 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 examples/quickstart for it as a runnable program.

Example (BackdatedCharges)

A test of a report needs charges at chosen times. Build the stores on a clock the test sets, record a capability that outlives the period, and move the clock forward before each charge: a charge carries the store's time, so it lands where the clock says, and no test writes a store's tables.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/google/uuid"

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

func main() {
	ctx := context.Background()
	start := time.Date(2026, time.March, 2, 9, 0, 0, 0, time.UTC) // a Monday
	at := start
	clock := func() time.Time { return at }
	records, err := memstore.NewStore(memstore.StoreConfig{Now: clock})
	if err != nil {
		log.Fatal(err)
	}
	usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records, Now: clock})
	if err != nil {
		log.Fatal(err)
	}
	ledger := memstore.NewLedger(usage)

	// The meter does not read expiry, but a capability that expired before
	// a charge's time would make a report the test cannot reproduce live.
	const period = 14 * 24 * time.Hour
	c := limes.Capability{
		ID: uuid.New(), IssuedAt: start, ExpiresAt: start.Add(period),
		Subject: limes.Principal{Type: limes.PrincipalTypeService, TenantID: uuid.New(), Subject: "svc:reporting"},
		Caveats: limes.Caveats{Requests: limes.RequestCeiling{Unlimited: true}, Budget: limes.BudgetCeiling{Unlimited: true, UnitCode: "USD"}},
	}
	if err := records.Insert(ctx, limes.InsertRequest{Capability: c}); err != nil {
		log.Fatal(err)
	}
	subject, err := limes.StoredMeterSubject(ctx, records, c.ID)
	if err != nil {
		log.Fatal(err)
	}
	for _, day := range []int{0, 1, 8} { // Monday, Tuesday, and Tuesday a week on
		at = start.AddDate(0, 0, day)
		if _, err := usage.Charge(ctx, limes.ChargeRequest{Subject: subject, Amount: limes.MustParseAmount("1.5")}, nil); err != nil {
			log.Fatal(err)
		}
	}

	page, err := ledger.SumCharges(ctx, limes.SumChargesRequest{
		TenantID: c.Subject.TenantID, From: start, To: start.Add(period), Bucket: limes.ChargeBucketWeek,
	})
	if err != nil {
		log.Fatal(err)
	}
	for _, sum := range page.Items {
		fmt.Println(sum.BucketStart.Format(time.DateOnly), sum.Charged, sum.Charges)
	}
}
Output:
2026-03-02 3 2
2026-03-09 1.5 1
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/dpop"
	"github.com/oleg-tkachuk/limes/jwks"
	"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.Verifier
	records  *memstore.Store
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	key, err := limes.GenerateKey()
	kid, pub, priv := key.KeyID, key.Public(), key.Private
	if err != nil {
		log.Fatal(err)
	}
	records, err := memstore.NewStore(memstore.StoreConfig{})
	if err != nil {
		log.Fatal(err)
	}
	usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
	if err != nil {
		log.Fatal(err)
	}
	signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: 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)
	}
	replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
	if err != nil {
		log.Fatal(err)
	}
	proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewVerifier(limes.VerifierConfig{
		Keys:               exampleKeys(kid, pub),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               proofs,
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: usage, tenantID: uuid.New(),
	}
}

// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
	vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
	if err != nil {
		log.Fatal(err)
	}
	return vc
}

// 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) {
	cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalTypeAgent, 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/"},
			Requests:         limes.RequestCeiling{Max: 500},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
		},
	})
	c, token := cResult.Capability, cResult.Token
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	_, token := s.issue()
	// The ceilings come from what the verifier accepted, never from the caller.
	subject := s.verified(token).MeterSubject()

	charge := func(amount string) (limes.ChargeReceipt, error) {
		return s.usage.Charge(ctx, limes.ChargeRequest{
			Subject: subject, Amount: limes.MustParseAmount(amount), Op: "search",
		}, nil)
	}

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

	// 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, Remaining: true})
	fmt.Println(err, refund.Amount)
}

// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
	keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
	if err != nil {
		log.Fatal(err)
	}
	return keys
}
Output:
<nil> 0.35
true
<nil> 0.35
Example (ProofOfPossession)
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/dpop"
	"github.com/oleg-tkachuk/limes/jwks"
	"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.Verifier
	records  *memstore.Store
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	key, err := limes.GenerateKey()
	kid, pub, priv := key.KeyID, key.Public(), key.Private
	if err != nil {
		log.Fatal(err)
	}
	records, err := memstore.NewStore(memstore.StoreConfig{})
	if err != nil {
		log.Fatal(err)
	}
	usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
	if err != nil {
		log.Fatal(err)
	}
	signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: 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)
	}
	replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
	if err != nil {
		log.Fatal(err)
	}
	proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewVerifier(limes.VerifierConfig{
		Keys:               exampleKeys(kid, pub),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               proofs,
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: usage, tenantID: uuid.New(),
	}
}

// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
	vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
	if err != nil {
		log.Fatal(err)
	}
	return vc
}

// 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) {
	cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalTypeAgent, 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/"},
			Requests:         limes.RequestCeiling{Max: 500},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
		},
	})
	c, token := cResult.Capability, cResult.Token
	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, _ := dpop.Thumbprint(agentKey.Public())
	tokenResult, err := s.issuer.Issue(ctx, limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject:  limes.Principal{Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "worker"},
		Audience: []string{exampleAudience}, TTL: 15 * time.Minute,
		Caveats:         limes.Caveats{Ops: []limes.Op{limes.OpGet}},
		ConfirmationJKT: jkt,
	})
	token := tokenResult.Token
	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, dpop.ErrRequired))

	// The client signs a fresh proof per request.
	proof, _ := dpop.Sign(ctx, agentKey, dpop.ProofRequest{Method: method, URL: url, Token: token, IssuedAt: time.Now()})
	req := limes.VerifyRequest{
		Token: token, Audience: exampleAudience,
		DPoP: dpop.Proof{JWT: 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, dpop.ErrReplayed))
}

// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
	keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
	if err != nil {
		log.Fatal(err)
	}
	return keys
}
Output:
true
<nil>
true
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/dpop"
	"github.com/oleg-tkachuk/limes/jwks"
	"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.Verifier
	records  *memstore.Store
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	key, err := limes.GenerateKey()
	kid, pub, priv := key.KeyID, key.Public(), key.Private
	if err != nil {
		log.Fatal(err)
	}
	records, err := memstore.NewStore(memstore.StoreConfig{})
	if err != nil {
		log.Fatal(err)
	}
	usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
	if err != nil {
		log.Fatal(err)
	}
	signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: 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)
	}
	replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
	if err != nil {
		log.Fatal(err)
	}
	proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewVerifier(limes.VerifierConfig{
		Keys:               exampleKeys(kid, pub),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               proofs,
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: usage, tenantID: uuid.New(),
	}
}

// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
	vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
	if err != nil {
		log.Fatal(err)
	}
	return vc
}

// 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) {
	cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalTypeAgent, 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/"},
			Requests:         limes.RequestCeiling{Max: 500},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
		},
	})
	c, token := cResult.Capability, cResult.Token
	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{
		CapabilityID: c.ID, Note: "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))
}

// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
	keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
	if err != nil {
		log.Fatal(err)
	}
	return keys
}
Output:
<nil>
true

Index

Examples

Constants

View Source
const (
	DefaultListLimit = 50
	MaxListLimit     = 500
)

Page sizes of a list: Limit 0 means DefaultListLimit, anything above MaxListLimit is clamped to it, and a negative Limit is the request's error (Validate).

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 DefaultGeneration = 1

DefaultGeneration is the Generation of a capability issued without one, and of every delegated capability.

View Source
const DefaultHousekeepingLimit = 1_000

DefaultHousekeepingLimit is how many rows one housekeeping batch — Store.Purge, UsageHousekeeping.ReleaseExpired or PurgeOrphans — handles when the request names no limit.

View Source
const DefaultIssueTTL = 15 * time.Minute

DefaultIssueTTL is IssuerConfig.DefaultTTL when it is zero.

View Source
const DefaultLeeway = 30 * time.Second

DefaultLeeway is VerifierConfig.Leeway when it is zero.

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 DefaultRealm = "limes"

DefaultRealm is the realm of a verifier whose config names none.

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 MaxBiscuitBlocks = 32

MaxBiscuitBlocks bounds the attenuation blocks a Biscuit may carry. Each block is a revocation id the verifier looks up, and anyone holding a copy can append blocks offline, so an unbounded chain is a lookup per block on every request. Attenuate refuses to go past it, and the verifier refuses a token that has.

View Source
const MaxCopyUsageIDs = MaxListLimit

MaxCopyUsageIDs is the most revocation ids one CopyUsage reads, as a page of any listing holds.

View Source
const MaxDelegationDepth = 64

MaxDelegationDepth is the most ancestors a capability may have: how far IsRevoked walks, and how deep Delegate mints.

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 = jose.MaxIJSONInteger

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 MaxLeeway = 5 * time.Minute

MaxLeeway is the widest VerifierConfig.Leeway a verifier takes: a token is accepted for at most this long past its expiry, which is what lets a store purge it safely after MinPurgeRetention.

View Source
const MinPurgeRetention = MaxLeeway + PurgeClockSkew

MinPurgeRetention is the shortest retention Purge takes: no verifier accepts a token longer than MaxLeeway past its expiry, by a clock no more than PurgeClockSkew behind the store's, so a capability expired for longer can lose its revocation entry unseen.

View Source
const NanosPerUnit = 1_000_000_000

NanosPerUnit is how many nanos make one unit.

View Source
const PurgeClockSkew = time.Minute

PurgeClockSkew is how far the clock of the store that purges may run ahead of a verifier's: the store measures expiry by its own clock, the verifier by its own, and a revocation must outlive the token on both.

Variables

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

Request-time caveat rejections. Each also matches ErrCaveatViolation.

View Source
var (
	// ErrUnauthenticated is the family of every refusal of the presented
	// credentials, token or proof: the one errors.Is a consumer needs to
	// answer them all as unauthenticated.
	ErrUnauthenticated = errors.New("limes: unauthenticated")
	// ErrInvalidToken — the token is not genuine or not this verifier's
	// to accept: a bad signature, an unknown kid, an untrusted issuer, a
	// malformed header or claims. One sentinel for all, so a caller cannot
	// tell an attacker which check failed.
	ErrInvalidToken error = &familyError{"limes: invalid token", ErrUnauthenticated}
	// ErrProofOfPossession is the family of every proof-of-possession
	// refusal: the token may be genuine, the proof that its holder sent it
	// is what failed. The verifier wraps each dpop refusal in it.
	ErrProofOfPossession error = &familyError{"limes: proof of possession refused", ErrUnauthenticated}
	ErrExpired           error = &familyError{"limes: expired", ErrUnauthenticated}
	ErrNotYetValid       error = &familyError{"limes: not yet valid", ErrUnauthenticated}
	ErrRevoked           error = &familyError{"limes: revoked", ErrUnauthenticated}
	// ErrDelegationTooDeep — a capability with more than MaxDelegationDepth
	// ancestors on record. Its root lies past the bounded IsRevoked walk,
	// so a revocation there would go unseen: it is refused as revoked,
	// never accepted, and Delegate refuses to mint one. A store answers it
	// as an error, which no revocation cache keeps.
	ErrDelegationTooDeep error = &familyError{"limes: delegation chain too deep", ErrRevoked}
	ErrAudienceMismatch  error = &familyError{"limes: audience mismatch", ErrUnauthenticated}
	ErrCaveatViolation         = errors.New("limes: caveat violation")
	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 a request 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 or an amount that do not
	// validate. Nothing was done. Every such error matches it, so a caller
	// maps the whole family to its "bad request" without listing it. A
	// request that is well formed but asks for more than is allowed has its
	// own sentinel instead: ErrDelegationTooWide, ErrUnitCodeMismatch.
	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")
	// ErrScopeMismatch is a store call whose context is scoped to a tenant
	// other than the one its request names — or to none — under a store
	// that keeps each tenant's rows apart. The rows are there; the call may
	// not see them. It is the caller's wiring that is wrong, not the
	// request, so it matches neither ErrInvalidRequest nor ErrNotFound, and
	// ErrorResponse answers it 500.
	ErrScopeMismatch = errors.New("limes: call scoped to another tenant than its request names")
	// ErrProofUnsupported is a key-bound capability presented to a verifier
	// with no DPoP configured: no proof the client sends could be checked.
	// It is the deployment's fault, not the request's, so it matches no
	// ErrUnauthenticated sentinel and ErrorResponse invites no retry.
	ErrProofUnsupported = errors.New("limes: key-bound capability, and this verifier checks no proof")
)

Errors exposed to callers, which switch on them with errors.Is. A consumer maps them to its transport, or lets Verifier.ErrorResponse map them to HTTP.

Some are families that more specific sentinels match as well, so a caller branches on the one it needs:

ErrUnauthenticated       the presented credentials were refused (401)
├── ErrNoCredentials     none were presented
├── ErrInvalidToken      the token is not genuine, or not this verifier's
│   └── ErrBiscuitAttenuation
├── ErrProofOfPossession the proof of possession is missing or wrong
│   └── ErrAuthSchemeMismatch
├── ErrRevoked
│   └── ErrDelegationTooDeep
└── ErrExpired, ErrNotYetValid, ErrAudienceMismatch
ErrCaveatViolation       a caveat refuses the operation (403)
ErrBudgetExceeded, ErrTenantBudgetExceeded, ErrRequestLimitExceeded
                         a ceiling refuses the request (403)
ErrInvalidRequest        the request itself is malformed
├── ErrInvalidCaveats, ErrInvalidAmount, ErrEffectConflict
└── ErrMalformedAuthorization  the client's Authorization header (400)
ErrNotFound              what the request names is not on record
└── ErrChargeNotFound, ErrReservationNotFound, ErrUsageNotFound, …
View Source
var (
	// ErrInvalidAmount — an amount that is negative or above MaxNanos, or a
	// decimal that is not one (ParseAmount). Refunds are explicit; a
	// negative charge is not one. A malformed request, so it matches
	// ErrInvalidRequest.
	ErrInvalidAmount error = &familyError{"limes: invalid amount", ErrInvalidRequest}

	// ErrCounterOverflow — an amount, each valid on its own, that would take
	// a counter past MaxNanos. The request is well formed; the counter has
	// no room for it.
	ErrCounterOverflow = errors.New("limes: counter would pass MaxNanos")

	// ErrChargeNotFound — Refund, GetCharge or ChargeByRef named a charge
	// with no ledger row.
	ErrChargeNotFound error = &familyError{"limes: charge not found", ErrNotFound}

	// 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 error = &familyError{"limes: reservation not found", ErrNotFound}

	// ErrRequestLimitExceeded — the capability, a Biscuit copy or an
	// ancestor used every request its ceiling allows; the next 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 error = &familyError{"limes: usage row not found", ErrNotFound}

	// ErrTenantBudgetExceeded — the tenant aggregate cap has been
	// hit. Distinct from ErrBudgetExceeded because the remedy differs:
	// an operator raises a tenant's ceiling with SetTenantBudget, while a
	// capability's ceiling is in its token and lifting it means issuing a
	// new one.
	ErrTenantBudgetExceeded = errors.New("limes: tenant aggregate budget exceeded")

	// ErrTenantBudgetNotFound — Get-only; the tenant has no ceiling set
	// and has never been charged. A tenant charged before any ceiling was
	// set has a row, Unlimited, with ResourceVersion 0.
	ErrTenantBudgetNotFound error = &familyError{"limes: tenant budget row not found", ErrNotFound}

	// 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 lives in types.go.

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

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

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

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 ErrProofOfPossession: the binding, not the token, is what the scheme fails.

View Source
var ErrBiscuitAttenuation error = &familyError{"limes: invalid Biscuit attenuation", ErrInvalidToken}

ErrBiscuitAttenuation — an attenuation block the verifier cannot honour: outside the vocabulary, malformed, or widening its parent. Matches ErrInvalidToken: the token is not one this verifier accepts.

View Source
var ErrCopyCountersNotMetered = errors.New("limes: copy limits need a verifier with MeterCopies")

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. The token is not at fault — a verifier with MeterCopies accepts it — so this is the server's configuration, outside ErrInvalidToken: ErrorResponse answers it as an internal error, and it has no Reason.

View Source
var ErrCursorRepeated = errors.New("limes: a page returned a cursor already fetched")

ErrCursorRepeated is a page that hands back a cursor the walk has already fetched — its own, or one further back. Following it would fetch the same pages forever, so the walk stops there.

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

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 = &familyError{"limes: external ref already names a different charge", ErrInvalidRequest}

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 = &familyError{"limes: invalid caveats", ErrInvalidRequest}

ErrInvalidCaveats is returned when a caveat bag is malformed: a budget outside 0..MaxNanos, a request limit outside 0..MaxIJSONInteger, 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 ErrMalformedAuthorization error = &familyError{"limes: malformed Authorization header", ErrInvalidRequest}

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, so it matches ErrInvalidRequest; ErrorResponse still answers it 400, as the client's fault, where the rest of that family is the server's own call.

View Source
var ErrNoCredentials error = &familyError{"limes: no credentials", ErrUnauthenticated}

ErrNoCredentials — the request carries no credential this module reads: no Authorization header, or one under a scheme other than Bearer or DPoP. RFC 6750 §3.1 answers it with a challenge and no error code: the client has not tried, so nothing it sent was wrong.

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

ErrNotFound is the family of every not-found error: Store.GetRecord returns it, and the meter's not-found sentinels (ErrUsageNotFound and the others) match it under errors.Is. 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.

The verifier never returns it: an id not on record is simply not revoked.

Functions

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/dpop"
	"github.com/oleg-tkachuk/limes/jwks"
	"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.Verifier
	records  *memstore.Store
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	key, err := limes.GenerateKey()
	kid, pub, priv := key.KeyID, key.Public(), key.Private
	if err != nil {
		log.Fatal(err)
	}
	records, err := memstore.NewStore(memstore.StoreConfig{})
	if err != nil {
		log.Fatal(err)
	}
	usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
	if err != nil {
		log.Fatal(err)
	}
	signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: 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)
	}
	replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
	if err != nil {
		log.Fatal(err)
	}
	proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewVerifier(limes.VerifierConfig{
		Keys:               exampleKeys(kid, pub),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               proofs,
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: usage, tenantID: uuid.New(),
	}
}

// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
	vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
	if err != nil {
		log.Fatal(err)
	}
	return vc
}

// 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) {
	cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalTypeAgent, 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/"},
			Requests:         limes.RequestCeiling{Max: 500},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
		},
	})
	c, token := cResult.Capability, cResult.Token
	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.PrincipalTypeAgent, Subject: "research-orchestrator",
		Limit: pageSize,
	}) {
		if err != nil {
			log.Fatal(err)
		}
		n++
	}
	fmt.Println(n)
}

// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
	keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
	if err != nil {
		log.Fatal(err)
	}
	return keys
}
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/dpop"
	"github.com/oleg-tkachuk/limes/jwks"
	"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.Verifier
	records  *memstore.Store
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	key, err := limes.GenerateKey()
	kid, pub, priv := key.KeyID, key.Public(), key.Private
	if err != nil {
		log.Fatal(err)
	}
	records, err := memstore.NewStore(memstore.StoreConfig{})
	if err != nil {
		log.Fatal(err)
	}
	usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
	if err != nil {
		log.Fatal(err)
	}
	signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: 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)
	}
	replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
	if err != nil {
		log.Fatal(err)
	}
	proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewVerifier(limes.VerifierConfig{
		Keys:               exampleKeys(kid, pub),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               proofs,
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: usage, tenantID: uuid.New(),
	}
}

// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
	vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
	if err != nil {
		log.Fatal(err)
	}
	return vc
}

// 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) {
	cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalTypeAgent, 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/"},
			Requests:         limes.RequestCeiling{Max: 500},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
		},
	})
	c, token := cResult.Capability, cResult.Token
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

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

	fullResult, err := s.issuer.Biscuit(context.Background(), limes.BiscuitRequest{Capability: s.verified(token)})
	full := fullResult.Token
	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.Capability().ID == c.ID, got.Capability().Caveats.Ops, got.Capability().Caveats.ResourcePrefixes)
}

// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
	keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
	if err != nil {
		log.Fatal(err)
	}
	return keys
}
Output:
<nil> true [get] [corpus/public/2026/]

func ExactPercent added in v0.50.1

func ExactPercent(pct float64) *big.Rat

ExactPercent is pct as the decimal it is written as — the shortest decimal that reads back as the same float64 — so a threshold of 1.1 means eleven tenths exactly, not the binary fraction a hair above it. A store compares spent / max × 100 against it without rounding. It is nil for NaN or an infinity, which ListTenantBudgetsRequest.Validate refuses.

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: it names no unit, and no unit is ever assumed for it.

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, between slashes that may be encoded or backslashes — 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 PageLimit

func PageLimit(requested int) int

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

func ValidateExternalRef

func ValidateExternalRef(ref string) error

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

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
	// AgentVersion pins the build / git-sha of the agent runtime.
	AgentVersion string
	// RunID is a UUID minted by the agent host at session start.
	RunID uuid.UUID
	// ParentAgentID is set when this agent was spawned by another via
	// `OpShare`. Empty for top-level agents.
	ParentAgentID uuid.UUID
	// Model identifies the LLM behind the agent ("example-model-1",
	// "gpt-5-pro"). Used for cost attribution.
	Model string
	// MCPClient is the agent host's user-agent ("example-host@1.4.0",
	// "cursor@0.44"). Useful for compatibility tracking.
	MCPClient string
	// contains filtered or unexported fields
}

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
	// Audience the token keeps.
	Audience []string
	// ExpiresAt, when earlier than the token's, becomes its expiry.
	ExpiresAt time.Time
	// ConfirmationJKT binds the token to a key (dpop.Thumbprint). 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
	// Requests and Budget 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, which a block does not
	// carry — so Budget names no UnitCode, and the verifier gives it the
	// capability's. nil sets none; a ceiling of 0 freezes the copy. Each
	// must be within every limit already in force, and an Unlimited one is
	// refused: a copy only narrows.
	Requests *RequestCeiling
	Budget   *BudgetCeiling
}

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 — a gRPC metadata entry, a message field.
	// VerifyRequest checks no scheme for it; a key-bound token still needs
	// its DPoP proof.
	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. With ErrMalformedAuthorization it still returns the scheme it recognised, so ErrorResponse challenges under the scheme the client used.

func (AuthScheme) Validate added in v0.37.0

func (s AuthScheme) Validate() error

Validate refuses a scheme other than the ones ParseAuthorization returns, or AuthSchemeUnspecified, as ErrInvalidRequest.

type AuthorizeRequest added in v0.3.0

type AuthorizeRequest struct {
	// Verify is the token and how it was presented.
	Verify VerifyRequest
	// Check is what the token is asked to allow.
	Check 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 {
	// contains filtered or unexported fields
}

BiscuitCopy identifies one copy of a capability's Biscuit. Only Verifier.BiscuitCopy makes one, from a token whose signature chain it checked, so the revocation id and the capability it is revoked under always belong together: a caller cannot name another tenant's copy under its own capability. The zero BiscuitCopy names nothing.

func (BiscuitCopy) CapabilityID

func (c BiscuitCopy) CapabilityID() uuid.UUID

CapabilityID is the capability the Biscuit seals.

func (BiscuitCopy) Copies added in v0.50.0

func (c BiscuitCopy) Copies() []CopyCeiling

Copies 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.

func (BiscuitCopy) RevocationID

func (c BiscuitCopy) RevocationID() []byte

RevocationID is the id of the copy's last block: listing it revokes this copy and every copy attenuated from it.

type BiscuitRequest added in v0.46.0

type BiscuitRequest struct {
	// Capability is the capability to give a Biscuit form, as the verifier
	// accepted it.
	Capability VerifiedCapability
}

BiscuitRequest is the input shape for Issuer.Biscuit.

type BiscuitResult added in v0.46.0

type BiscuitResult struct {
	// Token is the Biscuit, base64url.
	Token string
	// contains filtered or unexported fields
}

BiscuitResult is what Issuer.Biscuit mints.

type BiscuitRevocation

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

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

type BiscuitRevocationCacheConfig added in v0.13.0

type BiscuitRevocationCacheConfig struct {
	// Upstream answers what the cache does not hold. Required.
	Upstream   BiscuitRevocationLookup
	TTL        time.Duration
	MaxEntries int
	Now        func() time.Time
}

BiscuitRevocationCacheConfig configures a CachedBiscuitRevocationChecker; its fields read as RevocationCacheConfig's do, MaxEntries counting revocation ids.

type BiscuitRevocationLookup

type BiscuitRevocationLookup interface {
	// IsBiscuitRevoked reports whether any of revocationIDs — the blocks
	// of one copy, authority first — has been revoked.
	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 the revocation id of one Biscuit copy, under the
	// capability the copy belongs to — the pair Verifier.BiscuitCopy read
	// from the token, which no caller can assemble. 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 ErrInvalidRequest for a request naming no copy.
	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 BudgetCeiling added in v0.27.0

type BudgetCeiling struct {
	// Max is the most that may be spent; 0 allows nothing. Within
	// 0..MaxNanos, and 0 when Unlimited.
	Max Nanos
	// Unlimited lifts the ceiling.
	Unlimited bool
	// UnitCode is the unit Max counts in: an ISO 4217 code (IsUnitCode),
	// AbstractUnitCode for a budget that is not money. Required when the
	// ceiling lets anything be spent; may be empty on the zero ceiling.
	UnitCode string
}

BudgetCeiling bounds an amount spent: at most Max of UnitCode, or any amount of it when Unlimited. The unit is part of the bound, as a currency is part of an amount: a ceiling that lets anything be spent names what, and the zero ceiling, which lets nothing be, needs none.

func (BudgetCeiling) Admits added in v0.27.0

func (c BudgetCeiling) Admits(amount Nanos) bool

Admits reports whether amount spent in all stays under the ceiling.

func (BudgetCeiling) String added in v0.27.0

func (c BudgetCeiling) String() string

String is the ceiling for logs and errors: its Max, or "unlimited", and its unit when it names one.

func (BudgetCeiling) Validate added in v0.27.0

func (c BudgetCeiling) Validate() error

Validate refuses a Max beside Unlimited, a unit that is not an ISO 4217 code, and a ceiling that lets something be spent without naming its unit, as ErrInvalidRequest; a Max outside 0..MaxNanos is ErrInvalidAmount (Nanos.Validate), which matches ErrInvalidRequest too.

func (BudgetCeiling) Within added in v0.27.0

func (c BudgetCeiling) Within(outer BudgetCeiling) bool

Within reports whether c allows no more than outer, by amount alone: whether the two count in one unit is the caller's to check, and to refuse as ErrUnitCodeMismatch rather than as a wider ceiling.

type BumpReceipt added in v0.16.0

type BumpReceipt struct {
	// RequestCount is the capability's request count after this one.
	RequestCount int64
	// contains filtered or unexported fields
}

BumpReceipt is what a Bump counted.

type BumpRequest

type BumpRequest struct {
	// Subject is the capability counted, with its ceiling and copy limits:
	// VerifiedCapability.MeterSubject. Ancestors' ceilings come from their
	// records.
	Subject MeterSubject
}

BumpRequest is the input to Meter.Bump.

func (BumpRequest) Validate added in v0.68.0

func (r BumpRequest) Validate() error

Validate refuses a request whose subject names no capability, no tenant or an unknown unit.

type CachedBiscuitRevocationChecker

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

CachedBiscuitRevocationChecker caches IsBiscuitRevoked answers per revocation id, under the same TTL, bound and sharing of concurrent misses as CachedRevocationChecker. A token is revoked when any of its ids is, so each id is asked about on its own: the ids a token shares with the copies it was attenuated from stay cached, and a fresh attenuation — which anyone holding a copy can make offline — costs one upstream lookup, for its new block, rather than one for the whole token. A token none of whose ids is cached costs one lookup per block.

A revoked id's cached "live" answer lasts until the TTL, as in CachedRevocationChecker; Clear drops every entry at once.

func NewCachedBiscuitRevocationChecker

func NewCachedBiscuitRevocationChecker(cfg BiscuitRevocationCacheConfig) (*CachedBiscuitRevocationChecker, error)

NewCachedBiscuitRevocationChecker wires a cache over cfg.Upstream, as NewCachedRevocationChecker does.

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 answers for each id in turn — a fresh cached answer, or one upstream lookup shared by every concurrent caller asking about that id — and stops at the first revoked. 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 — or when Clear drops it. There is no per-id drop: the cache does not know which entries are a revoked id's descendants.

Concurrent misses for the same id share one upstream call.

func NewCachedRevocationChecker

func NewCachedRevocationChecker(cfg RevocationCacheConfig) (*CachedRevocationChecker, error)

NewCachedRevocationChecker wires a cache over cfg.Upstream. A negative TTL or bound is refused; a deployment that wants no cache passes its store to the verifier directly.

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) 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: a
	// verifier refuses a token whose audience does not include the one it
	// verifies for (VerifyRequest.Audience).
	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 (package dpop). 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
}

Capability is the in-memory representation of a token. The wire form is a JWT; this struct is what the Issuer fills before signing, and what Verifier.Verify returns inside a VerifiedCapability.

func (Capability) Clone added in v0.24.0

func (c Capability) Clone() Capability

Clone is c with slices and pointers of its own: a store keeps, and hands out, a capability no caller can alter through a slice it kept — the record is append-only.

type CapabilityLister added in v0.2.0

type CapabilityLister interface {
	// ListByPrincipal returns one page (Store.ListByPrincipal).
	ListByPrincipal(ctx context.Context, req ListByPrincipalRequest) (Page[Capability], 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

	// 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

	// ResourceURIs pins the capability to specific URIs. Used by
	// hand-off flows where the parent passes exactly the artifacts the
	// child needs.
	ResourceURIs []string

	// Requests caps the total number of times the capability may be
	// used within its TTL; the zero value allows none, and
	// RequestCeiling{Unlimited: true} lifts the cap.
	Requests RequestCeiling

	// Budget is the cost budget the capability authorises, exact to the
	// nano, in Budget.UnitCode — the unit every amount metered against the
	// capability counts in. The zero value allows no spend and needs no
	// unit: the capability counts requests (Meter.Bump) but no amount, and
	// a Charge, Reserve or Settle on it with no unit is ErrInvalidRequest.
	// BudgetCeiling{Unlimited: true, UnitCode: u} lifts the bound. Once it
	// is exhausted, a charge returns ErrBudgetExceeded. A delegated child
	// whose budget names no unit takes its parent's.
	Budget BudgetCeiling

	// 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

	// IdempotencyKeyRequired forces the bearer to send an explicit
	// idempotency key on mutating operations (CheckRequest.Mutating).
	// Enforced by Caveats.Check from CheckRequest.HasIdempotencyKey.
	IdempotencyKeyRequired bool

	// 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
}

Caveats is a typed bag of restrictions, AND-combined. Ops is required; an empty resource or source-network list places no restriction on its axis. The ceilings are explicit: a zero RequestCeiling or BudgetCeiling allows nothing, and only Unlimited lifts one.

It is written into a token as the limes_caveats claim (wire.go), 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. Issuer.Delegate enforces this through Narrows before it signs, and the verifier does for every Biscuit attenuation block, so an attempt to widen is refused, never minted.

func (Caveats) AllowsResource

func (c Caveats) AllowsResource(uri string) bool

AllowsResource reports whether uri is reachable under the resource caveats: unrestricted caveats allow any uri; restricted ones require an exact ResourceURIs entry or a covering ResourcePrefixes entry. An empty uri, or one with a "." or ".." path segment, 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 ChargeBucket added in v0.61.0

type ChargeBucket uint8

ChargeBucket is how SumCharges splits a period in time.

const (
	// ChargeBucketUnspecified takes the default, ChargeBucketNone: a request
	// that names no bucket sums its period whole.
	ChargeBucketUnspecified ChargeBucket = iota
	ChargeBucketHour
	ChargeBucketDay
	ChargeBucketMonth
	// ChargeBucketWeek is an ISO week, from Monday 00:00 UTC.
	ChargeBucketWeek
	// ChargeBucketNone sums the period whole, in one bucket.
	ChargeBucketNone
)

The buckets, each cut at UTC boundaries. A new one is appended, so that no bucket's number changes.

func (ChargeBucket) Start added in v0.61.0

func (b ChargeBucket) Start(t time.Time) time.Time

Start is where the bucket holding t begins, in UTC; the zero time for ChargeBucketNone, which holds the whole period, and for ChargeBucketUnspecified, which takes it.

func (ChargeBucket) String added in v0.61.0

func (b ChargeBucket) String() string

String is the bucket's name.

func (ChargeBucket) Validate added in v0.61.0

func (b ChargeBucket) Validate() error

Validate refuses a bucket this module does not define.

type ChargeByRefRequest added in v0.31.0

type ChargeByRefRequest struct {
	// CapabilityID is the capability the charge was taken against.
	CapabilityID uuid.UUID
	// ExternalRef is the ChargeRequest.ExternalRef it was taken under.
	ExternalRef string
}

ChargeByRefRequest names one charge by the reference it was taken under.

type ChargeReceipt

type ChargeReceipt struct {
	// ChargeID identifies the ledger row; pass it to Meter.Refund.
	ChargeID uuid.UUID
	// SpentAmount is the capability's spend as this call returns (its subtree's,
	// when it has delegated children).
	SpentAmount 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 OverrunPolicyRecord allows.
	Overrun bool
	// TenantID and UnitCode are the charge's tenant and unit, so a caller
	// holding only the receipt — logged, or passed on — can attribute it. A
	// refusal by a ceiling or by onCharged still sets them.
	TenantID uuid.UUID
	UnitCode string
	// contains filtered or unexported fields
}

ChargeReceipt is the result of a committed charge.

type ChargeRecord

type ChargeRecord struct {
	ID           uuid.UUID
	CapabilityID uuid.UUID
	TenantID     uuid.UUID
	// Amount is what the charge took; RefundedAmount is what has been returned
	// from it since.
	Amount         Nanos
	RefundedAmount Nanos
	UnitCode       string
	// ExternalRef is the charge's ChargeRequest.ExternalRef; empty for none.
	ExternalRef string
	// Overrun reports that the charge crossed a ceiling (OverrunPolicyRecord).
	Overrun bool
	// Op and Actor are what the charge was for and who made it, as the
	// request named them; At is when it was committed.
	Op    Op
	Actor string
	At    time.Time
	// contains filtered or unexported fields
}

ChargeRecord is one charge as the ledger holds it.

type ChargeRequest

type ChargeRequest struct {
	// Subject is the capability charged — its tenant, its own ceiling, its
	// unit and the presented copy's limits: VerifiedCapability.MeterSubject.
	// No conversion happens: a counter the charge moves — the
	// capability's, an ancestor's, the tenant's — that already counts in
	// another unit refuses it with ErrUnitCodeMismatch, and so does a
	// reservation.
	Subject MeterSubject
	// Amount is the cost, in the subject's unit. Within 0..MaxNanos
	// (Nanos.Validate).
	Amount Nanos
	// Op and Actor are stamped on the ledger row for attribution only;
	// both may be empty and neither affects enforcement.
	Op    Op
	Actor string
	// 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 (capability, 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.

func (ChargeRequest) Validate added in v0.68.0

func (r ChargeRequest) Validate() error

Validate refuses an amount out of range, an unknown overrun policy, an external ref past MaxExternalRefBytes, and a subject that names no capability, no tenant or no unit, or an unknown unit.

type ChargeSum added in v0.61.0

type ChargeSum struct {
	// BucketStart is where the bucket begins, in UTC; the zero time when
	// the request sums the period whole. A bucket at either end of the
	// period sums only the charges within it.
	BucketStart time.Time
	UnitCode    string
	Op          Op
	Actor       string
	// CapabilityID is the capability charged.
	CapabilityID uuid.UUID
	// Charged is what the group's charges took; Refunded, what has been
	// returned from them since. Charged less Refunded is the net.
	Charged  Nanos
	Refunded Nanos
	// Charges counts the group's charges.
	Charges int64
	// contains filtered or unexported fields
}

ChargeSum is one group's sums. The fields a request did not group by are zero.

type Charged added in v0.67.0

type Charged[TX any] struct {
	// Tx is the consumer's transaction handle the charge commits on.
	Tx TX
	// Receipt is the receipt the call returns once the charge commits:
	// ChargeID names the ledger row it writes. Replayed is never set, as a
	// replay runs no onCharged.
	Receipt ChargeReceipt
	// contains filtered or unexported fields
}

Charged is what Meter.Charge and Meter.Settle hand onCharged: the charge it runs inside, before the call commits it.

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 ContextSigner added in v0.14.0

type ContextSigner interface {
	crypto.Signer
	SignContext(ctx context.Context, rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error)
}

ContextSigner is a crypto.Signer that signs under a context, as a key held in a KMS or an HSM does for its call over the network. Signer uses SignContext when its key has it, so a slow or cancelled call is bounded by the request that asked for the token.

type CopyCeiling

type CopyCeiling struct {
	// RevocationID is the block's revocation id.
	RevocationID []byte
	// Requests bounds the requests made with the copy; nil when the block
	// set no request limit of its own. Never Unlimited: a copy only narrows.
	Requests *RequestCeiling
	// Budget bounds the copy's spend, in the capability's unit, which its
	// UnitCode carries; nil when the block set no budget of its own. Never
	// Unlimited.
	Budget *BudgetCeiling
}

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.

func (CopyCeiling) AdmitsBudget added in v0.52.0

func (c CopyCeiling) AdmitsBudget(amount Nanos) bool

AdmitsBudget reports whether amount spent with the copy in all stays under its own budget; with none of its own, any amount does.

func (CopyCeiling) AdmitsRequests added in v0.52.0

func (c CopyCeiling) AdmitsRequests(n int64) bool

AdmitsRequests reports whether n requests made with the copy in all stay under its own request limit; with none of its own, any number does.

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. It refuses what CopyUsageRequest.Validate
	// refuses.
	CopyUsage(ctx context.Context, req CopyUsageRequest) (CopyUsageResult, 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 CopyUsageRequest added in v0.52.0

type CopyUsageRequest struct {
	// RevocationIDs are the copies' revocation ids: at least one, at most
	// MaxCopyUsageIDs, none empty.
	RevocationIDs [][]byte
}

CopyUsageRequest names the copies CopyUsage reads.

func (CopyUsageRequest) Validate added in v0.52.0

func (r CopyUsageRequest) Validate() error

Validate refuses a request naming no copy, more than MaxCopyUsageIDs, or an empty revocation id, as ErrInvalidRequest.

type CopyUsageResult added in v0.52.0

type CopyUsageResult struct {
	// Copies are the counters of the copies that have been used.
	Copies []CopyUsage
	// contains filtered or unexported fields
}

CopyUsageResult is what CopyUsage read.

type CountRecordsRequest added in v0.64.0

type CountRecordsRequest struct {
	// TenantID is the tenant whose records are counted; AllTenants counts
	// every tenant's instead. Exactly one of them is required.
	TenantID   uuid.UUID
	AllTenants bool
	// ExpiringWithin is the window ExpiringSoon counts in: an active record
	// expiring within it of now. Zero counts none; negative is refused.
	ExpiringWithin time.Duration
	// ByTenant and ByPrincipalType group the counts by the record's tenant
	// and by its subject's principal type as well.
	ByTenant, ByPrincipalType bool
	// Limit is the page size; see PageLimit. Negative is refused.
	Limit int
	// Cursor is the one the previous page returned; empty for the first.
	Cursor string
}

CountRecordsRequest asks for one page of record counts.

func (CountRecordsRequest) Validate added in v0.64.0

func (r CountRecordsRequest) Validate() error

Validate refuses a request that names no tenant and not every tenant, or both, a negative window, or a negative page size.

type DelegateRequest

type DelegateRequest struct {
	// Parent is the capability the child narrows, as the verifier accepted
	// it: only Verifier.Verify and Authorize make one.
	Parent VerifiedCapability
	// Subject is who the child authorises; its TenantID defaults to the
	// parent's.
	Subject Principal
	// Audience defaults to the parent's, and must be a subset of it.
	Audience []string
	// Caveats are the child's; see InheritCaveats.
	Caveats Caveats
	// InheritCaveats copies the parent's caveats verbatim and ignores
	// Caveats. It must be asked for: Caveats with no Ops is refused, never
	// read as "inherit", which would replace every other field the caller
	// set — a budget of 2 would become the parent's 25 without a word.
	InheritCaveats bool
	// TTL defaults to IssuerConfig.DefaultTTL, and is cut so the child
	// expires no later than the parent.
	TTL time.Duration
	// NotBefore is raised, when earlier, to the parent's start: its
	// NotBefore, else its IssuedAt.
	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 DeleteUsageRequest added in v0.31.0

type DeleteUsageRequest struct {
	CapabilityID uuid.UUID
}

DeleteUsageRequest names the capability whose usage row DeleteUsage drops.

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. It errs away from the one
	// that tightens reads: a tainted resource is refused only to a read, so
	// a consumer-defined operation that reads and is left undeclared is
	// never held to AllowTaintedRead. Declare EffectRead for one.
	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.

func (Effect) Validate added in v0.37.0

func (e Effect) Validate() error

Validate refuses an effect this package does not define as ErrEffectConflict.

type EraseTenantRequest added in v0.60.0

type EraseTenantRequest struct {
	TenantID uuid.UUID
	// Note and Actor are written on every revocation entry, as
	// RevokeRequest's are.
	Note  string
	Actor string
}

EraseTenantRequest names the tenant EraseTenantRecords erases and who asked, recorded on each revocation it writes.

func (EraseTenantRequest) Validate added in v0.60.0

func (r EraseTenantRequest) Validate() error

Validate refuses a request naming no tenant.

type EraseTenantResult added in v0.60.0

type EraseTenantResult struct {
	// Revoked counts the capabilities it revoked: those not revoked before.
	Revoked int64
	// Removed counts the records it removed; Kept, those it left on record
	// for Purge.
	Removed int64
	Kept    int64
	// contains filtered or unexported fields
}

EraseTenantResult is what EraseTenantRecords did.

type EraseTenantUsageRequest added in v0.60.0

type EraseTenantUsageRequest struct {
	TenantID uuid.UUID
}

EraseTenantUsageRequest names the tenant EraseTenantUsage erases.

func (EraseTenantUsageRequest) Validate added in v0.60.0

func (r EraseTenantUsageRequest) Validate() error

Validate refuses a request naming no tenant.

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 HeldCopy added in v0.52.0

type HeldCopy struct {
	// RevocationID is the copy's revocation id.
	RevocationID []byte
	// Budget is the copy's own budget the hold counts against; nil when the
	// copy has none of its own.
	Budget *BudgetCeiling
}

HeldCopy is a Biscuit copy a hold is on: a hold counts no requests, so it carries the copy's budget alone.

type InsertRequest added in v0.15.0

type InsertRequest struct {
	Capability Capability
	IssuedBy   Principal
}

InsertRequest is a capability to record, and who asked for it.

func (InsertRequest) Validate added in v0.53.0

func (r InsertRequest) Validate() error

Validate refuses a capability with no id, as ErrInvalidRequest: nothing could read it back, and the zero id is what a root's ParentID holds.

type InstrumentOption added in v0.12.0

type InstrumentOption func(*meteringConfig)

InstrumentOption configures Instrument.

func WithMeterProvider added in v0.3.0

func WithMeterProvider(p metric.MeterProvider) InstrumentOption

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

func WithTenantAttribute added in v0.3.0

func WithTenantAttribute() InstrumentOption

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

type IssuanceStore added in v0.15.0

type IssuanceStore 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.
	//
	// req.IssuedBy is the principal that ASKED for the capability, which
	// is not its Subject (the principal it authorises) and not its Issuer
	// (the service that minted it). A delegated capability is requested by
	// its parent's holder; a root one by an operator. It is issuance
	// metadata, not a claim, so it travels beside the Capability rather
	// than in it, which would change the token's frozen wire format.
	//
	// An id already on record returns ErrAlreadyExists and changes nothing.
	// It refuses what InsertRequest.Validate refuses, a parent not on
	// record as ErrInvalidRequest, a child expiring after its parent — or
	// with no expiry under a parent that has one — or in another tenant as
	// ErrDelegationTooWide, and a child that would have
	// more than MaxDelegationDepth ancestors still on record — counted as
	// IsRevoked counts them — as ErrDelegationTooDeep: Purge and the
	// IsRevoked walk rely on a lineage that is whole, no deeper than the
	// walk reaches, expires from the leaves up and stays in one tenant, so a
	// store that scopes rows by tenant sees all of it. A refused record is
	// not written. A
	// store that knows tenants returns ErrUnknownTenant or ErrTenantDeleted
	// for a tenant it cannot mint for.
	Insert(ctx context.Context, req InsertRequest) 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),
	// bounded at MaxDelegationDepth ancestors: a capability with more
	// still on record returns ErrDelegationTooDeep, never false. An id not
	// on record is not revoked.
	IsRevoked(ctx context.Context, id uuid.UUID) (bool, error)
}

IssuanceStore is what an Issuer needs of storage: to record what it mints, and to know whether a parent it delegates from has been revoked.

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. Its Subject is
	// required — a capability nobody requested cannot be audited — and the
	// rest is recorded as given: an operator need not be a tenant's
	// principal.
	IssuedBy   Principal
	Audience   []string
	Caveats    Caveats
	TTL        time.Duration // 0 → DefaultTTL; must reach the next whole second
	NotBefore  time.Time     // zero → valid from issuance, no nbf claim
	Generation int64         // 0 → DefaultGeneration
	// ConfirmationJKT binds the capability to the key with this RFC 7638
	// thumbprint (dpop.Thumbprint); empty issues a bearer capability.
	ConfirmationJKT string
}

IssueRequest is the input shape for Issuer.Issue. Subject (with its TenantID), IssuedBy, Audience and Caveats are required; TTL, NotBefore, Generation and ConfirmationJKT are optional, and the issuer fills in ID, IssuedAt, ExpiresAt and Issuer, and Generation when it is zero. Caveats are validated (Caveats.Validate) before anything is persisted.

type IssueResult added in v0.46.0

type IssueResult struct {
	Capability Capability
	Token      string
	// contains filtered or unexported fields
}

IssueResult is what Issue and Delegate mint: the capability as recorded, to log or display, and its compact-form token, to ship to the agent.

type Issuer

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

Issuer mints capabilities, in this order:

  1. Validate the request (caveats sane, parent narrowing if delegated).
  2. Sign the token, then persist the record, and only then return the token.

Signing first means a capability whose token cannot be minted — a signer that fails, or a token past MaxTokenBytes — is never recorded. The token is returned only once its record is written: if the store write fails, the signed token is dropped, so no token leaves the issuer that audit could not find or Revoke, which refuses an id not on record, could not stop.

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, req BiscuitRequest) (BiscuitResult, 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.

Like Delegate it takes a VerifiedCapability, so what it signs is what this issuer signed and recorded before, never a value a caller assembled or widened. It refuses one another issuer named, one read from a Biscuit — wrapping a copy again would shed its attenuation and its revocation id; its holder attenuates it instead — and one that has expired or that the store reports revoked.

func (*Issuer) Delegate

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

Delegate mints a child capability narrower than the supplied parent, which the verifier accepted (VerifiedCapability), so its caveats are the ones its token carries. 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 (VerifiedCapability.FromBiscuit), which is ErrInvalidRequest, as Issuer.Biscuit answers it — its holder attenuates it instead. A parent another issuer named is ErrInvalidRequest: this issuer's store holds no record of it, so revoking it there would never reach the child. A child that would have more than MaxDelegationDepth ancestors is ErrDelegationTooDeep, refused by Store.Insert before it is on record. A parent whose caveats do not allow OpShare — the right to delegate — is ErrOpNotAllowed: delegating is the share operation, and a capability granted without it may not have the issuer mint a new principal any part of it. OpShare gates Delegate only: a Biscuit form of the capability (Issuer.Biscuit) is attenuated by whoever holds it, with no key and no OpShare, and a copy handed on is the same capability, not a new principal. Give a capability that must not be passed on no Biscuit form.

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/dpop"
	"github.com/oleg-tkachuk/limes/jwks"
	"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.Verifier
	records  *memstore.Store
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	key, err := limes.GenerateKey()
	kid, pub, priv := key.KeyID, key.Public(), key.Private
	if err != nil {
		log.Fatal(err)
	}
	records, err := memstore.NewStore(memstore.StoreConfig{})
	if err != nil {
		log.Fatal(err)
	}
	usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
	if err != nil {
		log.Fatal(err)
	}
	signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: 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)
	}
	replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
	if err != nil {
		log.Fatal(err)
	}
	proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewVerifier(limes.VerifierConfig{
		Keys:               exampleKeys(kid, pub),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               proofs,
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: usage, tenantID: uuid.New(),
	}
}

// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
	vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
	if err != nil {
		log.Fatal(err)
	}
	return vc
}

// 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) {
	cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalTypeAgent, 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/"},
			Requests:         limes.RequestCeiling{Max: 500},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
		},
	})
	c, token := cResult.Capability, cResult.Token
	if err != nil {
		log.Fatal(err)
	}
	return c, token
}

func main() {
	ctx := context.Background()
	s := newExampleStack()
	_, token := s.issue()
	// The orchestrator presents its token; the parent is what the verifier
	// accepted.
	parent := s.verified(token)

	worker := limes.Principal{Type: limes.PrincipalTypeAgent, TenantID: s.tenantID, Subject: "sub-worker"}
	childResult, 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/"},
			Requests:         limes.RequestCeiling{Max: 50},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("2.00"), UnitCode: "USD"},
		},
	})
	child := childResult.Capability
	fmt.Println(err, child.ParentID == parent.Capability().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},
			Budget: limes.BudgetCeiling{Max: limes.MustParseAmount("999"), UnitCode: "USD"},
		},
	})
	fmt.Println(errors.Is(err, limes.ErrDelegationTooWide))
}

// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
	keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
	if err != nil {
		log.Fatal(err)
	}
	return keys
}
Output:
<nil> true
true

func (*Issuer) Issue

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

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

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/jwks"
	"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()
	key, _ := limes.GenerateKey()
	kid, pub, priv := key.KeyID, key.Public(), key.Private

	records, _ := memstore.NewStore(memstore.StoreConfig{}) // your Store
	signer, _ := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: priv})
	issuer, _ := limes.NewIssuer(limes.IssuerConfig{
		Signer: signer, Store: records, IssuerName: exampleIssuerName,
	})
	verifier, _ := limes.NewVerifier(limes.VerifierConfig{
		Keys:           exampleKeys(kid, pub),
		Revocations:    records,
		TrustedIssuers: []string{exampleIssuerName},
	})

	cResult, err := issuer.Issue(ctx, limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalTypeAgent, 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/"},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
		},
	})
	c, token := cResult.Capability, cResult.Token
	if err != nil {
		log.Fatal(err)
	}

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

// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
	keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
	if err != nil {
		log.Fatal(err)
	}
	return keys
}
Output:
true true
true

type IssuerConfig

type IssuerConfig struct {
	// Signer mints tokens (NewSigner), over a key in memory or one held in
	// a KMS or an HSM.
	Signer *Signer

	// Store records what the issuer mints and answers whether a parent is
	// revoked. Any Store is one.
	Store IssuanceStore

	// 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.
	// Zero: DefaultIssueTTL, short enough that revocation propagation
	// latency rarely matters. Negative is refused.
	DefaultTTL time.Duration

	// MaxTokenBytes refuses to mint a token longer than this, which a
	// verifier with the same limit (VerifierConfig.MaxTokenBytes) would
	// refuse unread. Zero: DefaultMaxTokenBytes; negative is refused.
	MaxTokenBytes int

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

IssuerConfig is the wiring for NewIssuer.

type KeyResolver

type KeyResolver interface {
	// PublicKey returns the Ed25519 public key for the given kid.
	// Returns jwks.ErrUnknownKID if the key set does not contain the kid,
	// which the verifier reads as the token's fault (ErrInvalidToken).
	// Any other error is the resolver's: the verifier refuses the token
	// with it as it is, the server's fault rather than the token's.
	PublicKey(ctx context.Context, kid string) (ed25519.PublicKey, error)
}

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

Keys are Ed25519 for all of 1.x: the token format, not the key, names the algorithm, so another algorithm would be another token type (AcceptTokenTypes) rather than a key this interface returns.

type LedgerReader added in v0.61.0

type LedgerReader interface {
	// SumCharges returns one page of sums, each one group's: the charges of
	// req.TenantID taken in [req.From, req.To) — by the store's clock, as
	// ChargeRecord.At has it — grouped by unit always, by req.Bucket, and
	// by whichever of operation, actor and capability req names. Within a
	// bucket the largest charged come first, so the first page of Limit
	// rows is the top Limit; buckets come in time order. A refund counts in
	// the group of the charge it returns, whenever it is made: a closed
	// period's sums still move. A sum past MaxNanos is ErrCounterOverflow.
	SumCharges(ctx context.Context, req SumChargesRequest) (Page[ChargeSum], error)
}

LedgerReader sums a tenant's charges ledger over a period, for billing and reporting: totals, the largest spenders by operation, actor or capability, and series by hour, day, week or month. It reads the ledger the Meter writes, so a consumer reports from it rather than from a store's tables. It is a type of its own, beside UsageStore rather than one of its interfaces: a reporter holds it, a meter does not. memstore and pgstore each ship one; metertest checks it when its Env sets one.

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 — at their ExpiresAt, by the store's clock — 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 int
}

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 ListReservationsRequest added in v0.16.0

type ListReservationsRequest struct {
	CapabilityID uuid.UUID
	// Limit is the page size; see PageLimit. Negative is refused.
	Limit int
	// Cursor is the one the previous page returned; empty for the first.
	Cursor string
}

ListReservationsRequest asks for one page of a capability's reservations.

func (ListReservationsRequest) Validate added in v0.16.0

func (r ListReservationsRequest) Validate() error

Validate refuses a request with no capability or a negative page size.

type ListTenantBudgetsRequest

type ListTenantBudgetsRequest struct {
	// Filter picks the rows; the zero value lists every tenant.
	Filter TenantBudgetFilter
	// ThresholdPct is the utilization TenantBudgetFilterAtOrPast lists from,
	// 0..100. Any other filter takes none.
	ThresholdPct float64
	// Limit is the page size; see PageLimit. Negative is refused.
	Limit int
	// Cursor is the one the previous page returned; empty for the first.
	Cursor string
}

ListTenantBudgetsRequest is the input shape for UsageStore.ListTenantBudgets.

func (ListTenantBudgetsRequest) Validate added in v0.13.0

func (r ListTenantBudgetsRequest) Validate() error

Validate refuses a request no page can answer: a negative page size, an unknown filter, a threshold outside 0..100, or a threshold beside a filter that takes none.

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) (BumpReceipt, 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.Subject.Copies), innermost
	//      first → ErrBudgetExceeded.
	//   2. The capability's own ceiling (req.Subject.Budget()) →
	//      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, once every ceiling has passed. It receives the consumer's
	// transaction handle and the receipt the call will return (Charged), so
	// a side effect such as an outbox write commits with the spend and names
	// the ChargeID. The ledger row is written after onCharged returns: it is
	// not there yet to read. 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 — also once the capability is no longer on record,
	// as Settle replays a settled hold. The pair names one cost: arriving again with a
	// different Amount, UnitCode or TenantID it returns
	// ErrExternalRefConflict and moves nothing. With req.Overrun = OverrunPolicyRecord, 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, c Charged[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.Remaining 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, and a request naming neither an Amount nor
	// Remaining, or both, ErrInvalidRequest. 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 OverrunPolicyRecord, 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. So does a
	// reservation made by another capability or tenant, or under other Biscuit copies
	// than req.Subject carries: a hold is settled by the copy that made it,
	// so a copy cannot settle a sibling's hold past its own limits.
	Settle(ctx context.Context, req SettleRequest, onCharged func(ctx context.Context, c Charged[TX]) error) (ChargeReceipt, error)

	// Release ends a reservation without charging. Idempotent: releasing
	// one that is gone is a no-op. So is releasing a reservation made by
	// another capability or tenant, or under other Biscuit copies than req.Subject
	// carries: it is none of this subject's, and stays held, so a copy
	// cannot free a sibling's hold to spend past its own limits.
	Release(ctx context.Context, req ReleaseRequest) error
}

Meter enforces the request and budget ceilings (Caveats.Requests, Caveats.Budget) 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 (Store.Purge, after expiry) no longer bounds anything — its children expired with it, since a child cannot outlive its parent. The capability itself must be on record: Bump, Charge, Reserve and Settle refuse one that is not — never recorded, or purged — with ErrNotFound, since none of its ancestors' ceilings could be read. A cost reported late is charged only while the record lasts, so keep records (PurgeRequest.ExpiredFor) as long as costs may still arrive.

Copies of a Biscuit

A Biscuit's holder can give a copy limits of its own (CopyCeiling), which arrive through the Subject of BumpRequest, ChargeRequest and ReserveRequest (MeterSubject.Copies). 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. Each method first refuses a request whose Validate does, before it reads anything: a store that calls Validate enforces a rule added to it without a change of its own.

A store over a database holds at most one connection per call: the reads of the lineage, the ceilings and the ledger, the writes, and onCharged all run on the one transaction the call opens. A call that takes a second connection while it holds its transaction deadlocks once as many calls run as the pool has connections — each holds one and waits for another — which a large pool under a light load never shows. Run metertest against a pool of one or two connections to catch it.

type MeterReader added in v0.16.0

type MeterReader interface {
	// 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 an external
	// reference (ChargeRequest.ExternalRef), so a reporter can reconcile
	// its own records with the ledger. ErrChargeNotFound when there is none,
	// an empty reference included: no charge is taken under no name.
	ChargeByRef(ctx context.Context, req ChargeByRefRequest) (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, a page at a time, every reservation
	// GetReservation would return that was made against the capability
	// itself — not its delegated children — soonest to expire first, and
	// holds expiring at the same instant in ascending ID, compared bytewise.
	// It refuses what ListReservationsRequest.Validate refuses.
	ListReservations(ctx context.Context, req ListReservationsRequest) (Page[Reservation], 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)
}

MeterReader reads back what the Meter recorded: charges, reservations and counters. A reporter or an admin view depends on it alone.

type MeterSubject added in v0.17.0

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

MeterSubject is what a Meter counts a request against: a verified capability's id and tenant, its request and budget ceilings, its unit and the limits of the Biscuit copy presented. Only VerifiedCapability.MeterSubject and StoredMeterSubject make one, so a ceiling is the one a verified token carries or a RecordReader reads back — never a value a caller sets on the subject itself. A store reads it through its methods. The zero MeterSubject names no capability and no tenant; a Meter refuses it with ErrInvalidRequest rather than count it anywhere.

func StoredMeterSubject added in v0.33.0

func StoredMeterSubject(ctx context.Context, records RecordReader, capabilityID uuid.UUID) (MeterSubject, error)

StoredMeterSubject is what the Meter counts a capability against for a cost reported with no token in hand, read from the capability's record — records is the Store, or anything reading its records. It reads the record itself rather than take a Record, whose fields any caller can set, so the ceilings are the ones on record. A stored record names no Biscuit copy, so the subject carries none. A revoked capability still gives one: a cost already incurred belongs in the ledger. ErrNotFound when the capability is not on record.

func (MeterSubject) Budget added in v0.27.0

func (s MeterSubject) Budget() BudgetCeiling

Budget is the capability's own budget ceiling.

func (MeterSubject) CapabilityID added in v0.17.0

func (s MeterSubject) CapabilityID() uuid.UUID

CapabilityID is the capability the request counts against.

func (MeterSubject) Copies added in v0.17.0

func (s MeterSubject) Copies() []CopyCeiling

Copies are the limits of the Biscuit copy presented, a copy of them.

func (MeterSubject) Requests added in v0.27.0

func (s MeterSubject) Requests() RequestCeiling

Requests is the capability's own request ceiling.

func (MeterSubject) TenantID added in v0.17.0

func (s MeterSubject) TenantID() uuid.UUID

TenantID is the capability's tenant: its aggregate ceiling applies, and the ledger files the charge under it.

func (MeterSubject) UnitCode added in v0.17.0

func (s MeterSubject) UnitCode() string

UnitCode is the unit the capability counts in, its budget's; empty when that names none, and then it counts requests alone.

type Nanos

type Nanos int64

Nanos is an amount of a currency or metering unit (see BudgetCeiling.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 Nanos.Validate 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.

func (Nanos) Validate added in v0.12.0

func (n Nanos) Validate() error

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

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"     // read one resource
	OpPut     Op = "put"     // write one resource
	OpList    Op = "list"    // list resources under a prefix
	OpDelete  Op = "delete"  // delete a resource
	OpPresign Op = "presign" // mint a pre-signed URL for a resource
	OpTag     Op = "tag"     // label a resource
	OpSearch  Op = "search"  // query an index
	OpEmbed   Op = "embed"   // compute embeddings
	OpShare   Op = "share"   // delegate a sub-capability to another agent through the issuer; not needed to attenuate a Biscuit copy
	OpManage  Op = "manage"  // administer: tenants, policy
)

The built-in operations. Get, List, Search and Embed read; the rest may change state (Op.ResolveEffect).

func (Op) Builtin

func (op Op) Builtin() bool

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

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 (
	// OverrunPolicyUnspecified takes the default, OverrunPolicyReject: a
	// caller that says nothing never commits past a ceiling.
	OverrunPolicyUnspecified OverrunPolicy = iota
	// OverrunPolicyReject 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.
	OverrunPolicyReject
	// OverrunPolicyRecord 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 OverrunPolicyReject.
	OverrunPolicyRecord
)

func (OverrunPolicy) String added in v0.21.0

func (p OverrunPolicy) String() string

String names the policy for logs and errors.

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 Page added in v0.15.0

type Page[T any] struct {
	Items []T
	Next  string
}

Page is one page of a list: its items, and the cursor that fetches the next page — empty on the last one. The cursor is the store's own, opaque to the caller.

type Principal

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

Principal identifies the subject the capability is issued to. It is written into a token as the limes_principal claim (wire.go). Agent principals have a richer shape than service principals: they carry the run / tool-call lineage that audit and cost attribution need.

func (Principal) Clone added in v0.24.0

func (p Principal) Clone() Principal

Clone is p with agent details of its own: a store keeps, and hands out, a principal no caller can alter through a pointer it kept.

func (Principal) Validate added in v0.8.0

func (p Principal) Validate() error

Validate reports whether p names a principal a capability can be issued to: one of the principal kinds, spelled as the constants spell it, and agent details only on an agent. The kind is part of the token, so a kind outside the set would be signed and then read by every verifier as it pleased.

type PrincipalType

type PrincipalType string

PrincipalType discriminates Principal.Type.

const (
	PrincipalTypeUnspecified PrincipalType = ""
	PrincipalTypeUser        PrincipalType = "user"    // a person
	PrincipalTypeAgent       PrincipalType = "agent"   // an AI agent; Principal.Agent says which
	PrincipalTypeService     PrincipalType = "service" // a service account
)

The principal kinds. The zero value, PrincipalTypeUnspecified, names none and is refused wherever a kind is required.

func (PrincipalType) Validate added in v0.21.0

func (t PrincipalType) Validate() error

Validate reports whether t is one of the principal kinds, spelled as the constants spell it. Every error matches ErrInvalidRequest.

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"`
	// contains filtered or unexported fields
}

Problem is an RFC 9457 problem details object.

type PurgeOrphansRequest added in v0.16.0

type PurgeOrphansRequest struct {
	// Limit bounds the rows deleted. Zero: DefaultHousekeepingLimit;
	// negative is refused.
	Limit int
}

PurgeOrphansRequest is one batch of UsageHousekeeping.PurgeOrphans.

type PurgeOrphansResult added in v0.16.0

type PurgeOrphansResult struct {
	Purged int64
	// contains filtered or unexported fields
}

PurgeOrphansResult is what one PurgeOrphans deleted.

type PurgeRequest added in v0.15.0

type PurgeRequest struct {
	// ExpiredFor is how long a capability must have been expired to go.
	// Zero: MinPurgeRetention; anything shorter, negative included, is
	// refused.
	ExpiredFor time.Duration
	// Limit bounds the records removed. Zero: DefaultHousekeepingLimit; negative
	// is refused.
	Limit int
}

PurgeRequest is one batch of Store.Purge.

func (PurgeRequest) Retention added in v0.23.0

func (r PurgeRequest) Retention() time.Duration

Retention is how long a capability must have been expired to go: ExpiredFor, or MinPurgeRetention when it is zero.

func (PurgeRequest) Validate added in v0.15.0

func (r PurgeRequest) Validate() error

Validate refuses a retention shorter than MinPurgeRetention, other than zero, and a negative limit.

type PurgeResult added in v0.15.0

type PurgeResult struct {
	// Purged is the number of capability records removed.
	Purged int64
	// contains filtered or unexported fields
}

PurgeResult is what one Purge removed.

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 (
	ReasonNoCredentials          Reason = "no_credentials"
	ReasonMalformedAuthorization Reason = "malformed_authorization"
	ReasonInvalidToken           Reason = "invalid_token"
	ReasonKeysUnavailable        Reason = "keys_unavailable"
	ReasonExpired                Reason = "expired"
	ReasonNotYetValid            Reason = "not_yet_valid"
	ReasonRevoked                Reason = "revoked"
	ReasonAudienceMismatch       Reason = "audience_mismatch"
	ReasonBiscuitAttenuation     Reason = "biscuit_attenuation"
	ReasonProofRequired          Reason = "proof_required"
	ReasonNonceRequired          Reason = "nonce_required"
	ReasonAuthSchemeMismatch     Reason = "auth_scheme_mismatch"
	ReasonProofInvalid           Reason = "proof_invalid"
	ReasonProofReplayed          Reason = "proof_replayed"
	ReasonOpNotAllowed           Reason = "op_not_allowed"
	ReasonResourceNotAllowed     Reason = "resource_not_allowed"
	ReasonSourceIPNotAllowed     Reason = "source_ip_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. The one member of that family that is a refusal, ErrMalformedAuthorization, has its reason.

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 RecordCensus added in v0.64.0

type RecordCensus interface {
	// CountRecords returns one page of counts, each one group's: the
	// records of req.TenantID, or of every tenant with req.AllTenants,
	// grouped by tenant and by principal type when req asks, in that order,
	// each ascending. A group with no record has no row, so the census of
	// no record is an empty page.
	CountRecords(ctx context.Context, req CountRecordsRequest) (Page[RecordCount], error)
}

RecordCensus counts capability records by what they are now — active, expired or revoked — for a platform operator's report: the records of one tenant or of every tenant, whole or by tenant and by principal type, with how many expire soon. It reads the store a consumer issues into, so a report counts through it rather than through a store's tables, whose columns are the store's own; a view over those tables could freeze its columns, but not take the store's clock or share these rules with every store. It is a type of its own, beside Store: a reporter holds it, an issuer does not. memstore and pgstore each ship one; storetest checks it when its Env sets one.

Each record counts under exactly one disposition, by the store's clock, the one expiry and purging are measured by:

  • Revoked: the record, or an ancestor of it still on record, is revoked — what Store.IsRevoked answers, so a child of a revoked parent is revoked here, unlike ListByPrincipalRequest.IncludeRevoked, which reads a record's own entry. Revoked wins over expired.
  • Expired: not revoked, and its ExpiresAt is zero or not after now. A child expires no later than its parent (Store.Insert), so no ancestor need be read for it.
  • Active: neither.

A Biscuit copy's revocation is not a record's and counts nowhere.

type RecordCount added in v0.64.0

type RecordCount struct {
	TenantID      uuid.UUID
	PrincipalType PrincipalType
	// Total is every record of the group: Active, Expired and Revoked
	// together, each record in one of them.
	Total, Active, Expired, Revoked int64
	// Delegated counts the group's records with a parent, whatever their
	// disposition.
	Delegated int64
	// ExpiringSoon counts the active records expiring within the
	// request's ExpiringWithin of now: a part of Active.
	ExpiringSoon int64
	// contains filtered or unexported fields
}

RecordCount is one group's counts. The fields a request did not group by are zero.

type RecordReader added in v0.42.0

type RecordReader interface {
	// 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 when there is no record. For audit
	// and admin reads, and for a reporter that needs a capability's
	// ceilings; Verifier never calls it.
	GetRecord(ctx context.Context, id uuid.UUID) (Record, error)
}

RecordReader reads one capability's record. Store implements it; a caller that only reads records — StoredMeterSubject — asks for no more.

type RefundReceipt added in v0.3.0

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

RefundReceipt is the result of Meter.Refund.

type RefundRequest

type RefundRequest struct {
	ChargeID uuid.UUID
	// Amount to refund, above zero and within MaxNanos (Nanos.Validate).
	Amount Nanos
	// Remaining refunds whatever of the charge is not yet refunded, which
	// makes a full refund idempotent: once nothing remains, it returns
	// nothing.
	Remaining bool
}

RefundRequest is the input to Meter.Refund. It names either an Amount or Remaining, never both and never neither.

func (RefundRequest) Validate added in v0.19.0

func (r RefundRequest) Validate() error

Validate refuses a request that names no amount, or both an amount and Remaining.

type ReleaseExpiredRequest added in v0.16.0

type ReleaseExpiredRequest struct {
	// Limit bounds the reservations released. Zero:
	// DefaultHousekeepingLimit; negative is refused.
	Limit int
}

ReleaseExpiredRequest is one batch of UsageHousekeeping.ReleaseExpired.

type ReleaseExpiredResult added in v0.16.0

type ReleaseExpiredResult struct {
	Released int64
	// contains filtered or unexported fields
}

ReleaseExpiredResult is what one ReleaseExpired released.

type ReleaseRequest added in v0.16.0

type ReleaseRequest struct {
	// Subject is the capability releasing, as it reserved
	// (VerifiedCapability.MeterSubject). A reservation of another
	// capability, or of other copies, is left held.
	Subject       MeterSubject
	ReservationID uuid.UUID
}

ReleaseRequest names the reservation to release, and who releases it.

func (ReleaseRequest) Validate added in v0.68.0

func (r ReleaseRequest) Validate() error

Validate refuses a request whose subject names no capability, no tenant or an unknown unit.

type RequestCeiling added in v0.27.0

type RequestCeiling struct {
	// Max is the most requests allowed; 0 allows none. Within
	// 0..MaxIJSONInteger, and 0 when Unlimited.
	Max int64
	// Unlimited lifts the ceiling.
	Unlimited bool
}

RequestCeiling bounds a count of requests: at most Max, or none at all when Unlimited.

func (RequestCeiling) Admits added in v0.27.0

func (c RequestCeiling) Admits(n int64) bool

Admits reports whether n requests in all stay under the ceiling.

func (RequestCeiling) String added in v0.27.0

func (c RequestCeiling) String() string

String is the ceiling for logs and errors: its Max, or "unlimited".

func (RequestCeiling) Validate added in v0.27.0

func (c RequestCeiling) Validate() error

Validate refuses a negative or out-of-range Max, and a Max beside Unlimited, as ErrInvalidRequest.

func (RequestCeiling) Within added in v0.27.0

func (c RequestCeiling) Within(outer RequestCeiling) bool

Within reports whether c allows no more than outer: anything fits under an unlimited ceiling, and under a bounded one only a bounded ceiling no higher.

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    Op
	Actor string
	// Copies are the Biscuit copies the hold is also on, innermost first,
	// with the budget each is held against.
	Copies    []HeldCopy
	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 {
	// Subject is the capability the hold counts against
	// (VerifiedCapability.MeterSubject). The reservation keeps the
	// presented copy's limits, and Settle checks and charges them.
	Subject MeterSubject
	// Amount to hold, in the subject's unit. Within 0..MaxNanos
	// (Nanos.Validate).
	Amount Nanos
	// TTL is how long the hold lasts if never settled or released.
	// 0 = DefaultReservationTTL; negative is ErrInvalidRequest.
	TTL time.Duration
	// Op and Actor are carried onto the charge Settle records.
	Op    Op
	Actor string
}

ReserveRequest is the input to Meter.Reserve.

func (ReserveRequest) Validate added in v0.68.0

func (r ReserveRequest) Validate() error

Validate refuses an amount out of range, a subject that names no capability, no tenant or no unit, or an unknown unit, and a negative TTL.

type Revocation

type Revocation struct {
	RevokedAt time.Time
	Note      string
	Actor     string
	// Cascaded reports that the entry was written by a cascading revoke —
	// of this capability or of an ancestor.
	Cascaded bool
	// contains filtered or unexported fields
}

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

type RevocationCacheConfig added in v0.13.0

type RevocationCacheConfig struct {
	// Upstream answers what the cache does not hold. Required.
	Upstream RevocationLookup
	// TTL is how long an answer is served. Zero: DefaultRevocationCacheTTL.
	TTL time.Duration
	// MaxEntries bounds the cache. Zero: DefaultRevocationCacheEntries.
	MaxEntries int
	// Now is the clock. Nil: time.Now.
	Now func() time.Time
}

RevocationCacheConfig configures a CachedRevocationChecker.

type RevocationLookup

type RevocationLookup interface {
	// IsRevoked reports whether id, or any ancestor of it, is revoked. An
	// error is no answer: the verifier refuses rather than assume not.
	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 {
	// Copy is the copy to revoke, as Verifier.BiscuitCopy read it.
	Copy BiscuitCopy
	// Note and Actor are stored for audit, as on RevokeRequest.
	Note  string
	Actor string
}

RevokeBiscuitRequest is the input shape for BiscuitRevocationStore.RevokeBiscuit.

func (RevokeBiscuitRequest) Validate added in v0.43.0

func (r RevokeBiscuitRequest) Validate() error

Validate refuses a request naming no copy as ErrInvalidRequest.

type RevokeRequest

type RevokeRequest struct {
	CapabilityID uuid.UUID
	// Note is an operator-supplied label ("compromise", "rotation",
	// "policy-change"). Stored on the revocation row for audit.
	Note 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
	// Ceiling is the tenant's new ceiling, in the unit it names: the zero
	// value freezes the tenant's spend, in whatever unit it counts, and
	// BudgetCeiling{Unlimited: true, UnitCode: u} lifts it.
	Ceiling BudgetCeiling
	// PeriodEnd pins a closing time for the accounting window; zero leaves
	// it open-ended (typical for "just set the cap, I'll roll later").
	PeriodEnd time.Time
	// ResetSpend=true zeroes the spend and rolls PeriodStart to
	// now. false leaves the counter alone — the cap changes mid-
	// window.
	ResetSpend bool
	// ExpectedVersion is the optimistic-concurrency guard, checked
	// atomically with the write. 0 asserts no ceiling has been set yet (the
	// budget is missing, or holds only counted spend) and is itself a
	// conflict if one has, so two operators editing one ceiling cannot
	// overwrite each other unseen.
	ExpectedVersion int64
}

SetTenantBudgetRequest is the input for UsageStore.SetTenantBudget.

func (SetTenantBudgetRequest) Validate added in v0.39.0

func (r SetTenantBudgetRequest) Validate() error

Validate refuses a request with no tenant, a ceiling that does not validate — among them one that lets something be spent and names no unit — or a negative ExpectedVersion, as ErrInvalidRequest. A store calls it before reading the tenant's row, so a malformed request is refused as one, never as a version conflict.

type SettleRequest

type SettleRequest struct {
	// Subject is the capability settling, whose ceiling the charge meets
	// (VerifiedCapability.MeterSubject). A reservation of another
	// capability is ErrReservationNotFound.
	Subject       MeterSubject
	ReservationID uuid.UUID
	// Amount is the actual cost to charge. Within 0..MaxNanos (Nanos.Validate).
	Amount Nanos
	// Overrun is what happens when Amount would cross a ceiling.
	Overrun OverrunPolicy
}

SettleRequest is the input to Meter.Settle.

func (SettleRequest) Validate added in v0.68.0

func (r SettleRequest) Validate() error

Validate refuses a subject that names no capability, no tenant or no unit, or an unknown unit, an amount out of range, and an unknown overrun policy. The zero ReservationID is not refused here: it names no reservation, which Settle reports as ErrReservationNotFound.

type Signer

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

Signer holds the key an Issuer mints capability tokens with: it encodes a Capability as the token format and signs it with an Ed25519 key, which may be in memory or held in a KMS or an HSM behind crypto.Signer. It signs only for an Issuer (IssuerConfig.Signer). Verifiers pick up the public keys (statically or from a JWKS endpoint) and validate locally — no RPC per request.

func NewSigner added in v0.14.0

func NewSigner(cfg SignerConfig) (*Signer, error)

NewSigner builds a Signer, refusing an empty kid or a key that is not Ed25519.

func (Signer) Format added in v0.65.0

func (s Signer) Format(f fmt.State, verb rune)

Format prints s with its key redacted.

type SignerConfig added in v0.14.0

type SignerConfig struct {
	// KeyID is the kid tokens carry, by which verifiers find the key.
	// Required.
	KeyID string
	// Key signs. Its public key must be Ed25519: an ed25519.PrivateKey, or
	// a crypto.Signer over a key held elsewhere — one that also implements
	// ContextSigner is given the request's context. Required.
	Key crypto.Signer
}

SignerConfig configures a Signer.

func (SignerConfig) Format added in v0.65.0

func (cfg SignerConfig) Format(f fmt.State, verb rune)

Format prints cfg with its key redacted, whether the key is an ed25519.PrivateKey or a crypto.Signer that holds one.

func (SignerConfig) LogValue added in v0.77.3

func (cfg SignerConfig) LogValue() slog.Value

LogValue logs cfg with its key redacted, under any slog handler.

func (SignerConfig) MarshalJSON added in v0.77.3

func (cfg SignerConfig) MarshalJSON() ([]byte, error)

MarshalJSON encodes cfg with its key redacted.

type SigningKey added in v0.3.0

type SigningKey struct {
	KeyID   string
	Private ed25519.PrivateKey
}

SigningKey is an Ed25519 key and the kid it is published under. It holds the private key alone, and Public derives the public half from it, so the two cannot disagree.

func GenerateKey added in v0.12.0

func GenerateKey() (SigningKey, error)

GenerateKey makes a fresh Ed25519 signing key under the kid jwks.KeyID derives for it. It lives only as long as the process unless saved.

func LoadKey added in v0.13.0

func LoadKey(path string) (SigningKey, error)

LoadKey reads the PKCS#8 PEM signing key at path, under the kid jwks.KeyID derives from it; a deployment that names its kids sets SigningKey.KeyID on the result. For a key that lives only as long as the process, GenerateKey; a deployment that may have no key file chooses between the two itself, so a missing mount is never silently a fresh key.

func (SigningKey) Format added in v0.65.0

func (k SigningKey) Format(f fmt.State, verb rune)

Format prints k with its private key redacted.

func (SigningKey) LogValue added in v0.77.3

func (k SigningKey) LogValue() slog.Value

LogValue logs k with its private key redacted, under any slog handler.

func (SigningKey) MarshalJSON added in v0.77.3

func (k SigningKey) MarshalJSON() ([]byte, error)

MarshalJSON encodes k with its private key redacted: a SigningKey written as JSON, to a log or anywhere else, is not a way to carry the key.

func (SigningKey) Public added in v0.3.0

func (k SigningKey) Public() ed25519.PublicKey

Public is the public half of Private; nil when Private is not an Ed25519 private key.

type Store

type Store interface {
	IssuanceStore
	RecordReader

	// 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

	// Purge removes the capabilities expired for longer than the request's
	// retention (PurgeRequest.Retention) — their records, their revocation
	// entries and those of their Biscuit copies — at most req.Limit of
	// them, and reports how many records it removed. A housekeeping worker
	// calls it until a batch comes back short of the limit. The retention
	// is how long an expired capability stays on record for audit, and
	// never less than MinPurgeRetention: a verifier still accepts a token
	// for up to its Leeway past expiry, and a revocation purged before
	// that would let a revoked token through again.
	//
	// Purge never removes a capability while a child of it stays on record:
	// a child expires no later than its parent (Insert refuses one that
	// would not), and among records expiring together Purge takes the
	// deepest first. The IsRevoked walk and the Meter's lineage, which stop
	// at the first ancestor no longer on record, therefore miss no
	// revocation and no ceiling. Once records go, UsageHousekeeping.PurgeOrphans
	// releases what the meter still holds for them.
	Purge(ctx context.Context, req PurgeRequest) (PurgeResult, error)

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

Store persists capability records and revocation entries. Consumers supply the implementation: memstore ships an in-memory one, and the module github.com/oleg-tkachuk/limes/pgstore one over PostgreSQL.

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 SumChargesRequest added in v0.61.0

type SumChargesRequest struct {
	// TenantID is the tenant whose charges are summed. Required: a sum
	// across tenants is not this request.
	TenantID uuid.UUID
	// From and To bound the period, From included, To not; From before To.
	From, To time.Time
	// Bucket splits the period at UTC boundaries; ChargeBucketNone sums it
	// whole, and so does ChargeBucketUnspecified, which takes it.
	Bucket ChargeBucket
	// ByOp, ByActor and ByCapability group the sums by the charge's Op,
	// Actor and capability as well.
	ByOp, ByActor, ByCapability bool
	// Limit is the page size; see PageLimit. Negative is refused.
	Limit int
	// Cursor is the one the previous page returned; empty for the first.
	Cursor string
}

SumChargesRequest asks for one page of a tenant's charge sums.

func (SumChargesRequest) Validate added in v0.61.0

func (r SumChargesRequest) Validate() error

Validate refuses a request with no tenant, a period that is empty or unbounded, a bucket this module does not define, or a negative page size.

type TenantBudget

type TenantBudget struct {
	TenantID uuid.UUID
	// Ceiling bounds the tenant's spend in the current period, in the unit
	// the tenant counts in: Ceiling.UnitCode, which the first charge or
	// hold records when no ceiling named it. A tenant whose ceiling was
	// never set counts Unlimited.
	Ceiling     BudgetCeiling
	SpentAmount Nanos
	// ReservedAmount is held by the tenant's open reservations.
	ReservedAmount Nanos
	PeriodStart    time.Time
	// PeriodEnd is when the accounting window closes; zero for an
	// open-ended one.
	PeriodEnd time.Time
	// UpdatedAt is when the store last wrote the aggregate, by its clock.
	UpdatedAt time.Time
	// ResourceVersion is the OCC token a caller passes back as
	// SetTenantBudgetRequest.ExpectedVersion. Zero means no ceiling has
	// ever been set — the row, if there is one, holds only what Charge and
	// Reserve counted.
	ResourceVersion int64
	// contains filtered or unexported fields
}

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.

func (TenantBudget) Utilization added in v0.19.0

func (b TenantBudget) Utilization() (pct float64, limited bool)

Utilization is how much of the ceiling is spent, in percent, clamped to 0..100: a tenant past its ceiling, or with a ceiling of 0, is at 100. limited is false for an unlimited tenant, which has no utilization to report; pct is then 0.

type TenantBudgetFilter added in v0.47.0

type TenantBudgetFilter uint8

TenantBudgetFilter picks the rows ListTenantBudgets lists.

const (
	// TenantBudgetFilterUnspecified takes the default, TenantBudgetFilterAll: a
	// request that names no filter lists every tenant.
	TenantBudgetFilterUnspecified TenantBudgetFilter = iota
	// TenantBudgetFilterAll lists every tenant: no filter.
	TenantBudgetFilterAll
	// TenantBudgetFilterAtOrPast lists tenants with a ceiling spent to
	// ListTenantBudgetsRequest.ThresholdPct or past it.
	TenantBudgetFilterAtOrPast
	// TenantBudgetFilterUnlimited lists tenants with no ceiling.
	TenantBudgetFilterUnlimited
)

func (TenantBudgetFilter) String added in v0.47.0

func (f TenantBudgetFilter) String() string

String names the filter for logs and errors.

func (TenantBudgetFilter) Validate added in v0.47.0

func (f TenantBudgetFilter) Validate() error

Validate refuses a filter this package does not define, as ErrInvalidRequest.

type TenantBudgetLister added in v0.2.0

type TenantBudgetLister interface {
	// ListTenantBudgets returns one page (TenantBudgets.ListTenantBudgets).
	ListTenantBudgets(ctx context.Context, req ListTenantBudgetsRequest) (Page[TenantBudget], error)
}

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

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. It refuses what
	// SetTenantBudgetRequest.Validate refuses — a ceiling outside
	// 0..MaxNanos is ErrInvalidAmount — and changes nothing. A ceiling in a
	// unit 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; a consumer joins its
	// own tenant data to them by TenantID. Filter picks the rows:
	//   - TenantBudgetFilterUnspecified, TenantBudgetFilterAll: every one.
	//   - TenantBudgetFilterAtOrPast: rows with a ceiling — a bound or 0 — where
	//     spent / max * 100 >= ThresholdPct, exactly; a ceiling of 0 is
	//     spent through at any threshold. At a threshold of 0, every row
	//     with a ceiling.
	//   - TenantBudgetFilterUnlimited: rows whose ceiling is Unlimited.
	// Limit is the page size, as PageLimit reads it.
	// It refuses what ListTenantBudgetsRequest.Validate refuses.
	//
	// Rows come most at risk first. Ceilings of 0, which allow nothing,
	// lead; bounded ceilings follow by spent / max descending, compared
	// exactly as fractions — unclamped, so of two tenants past their
	// ceilings the further one leads; unlimited ceilings, with no utilization, trail. Within each,
	// ties go 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) (Page[TenantBudget], error)
}

TenantBudgets administers the tenant-aggregate ceilings Charge enforces.

type TenantRecordEraser added in v0.60.0

type TenantRecordEraser interface {
	// EraseTenantRecords revokes every capability of the tenant not revoked
	// yet, so no token of the tenant verifies again, and removes those of
	// its records Purge would remove at MinPurgeRetention — each with its
	// revocation entry and its Biscuit copies' — and no other: an id not on
	// record is not revoked, so a record removed while its token may still
	// verify would let that token through again. Purge takes the rest as
	// it expires; a capability with no expiry stays on record, revoked. It
	// changes no other tenant's records, and a second call revokes and
	// removes nothing the first left.
	EraseTenantRecords(ctx context.Context, req EraseTenantRequest) (EraseTenantResult, error)
}

TenantRecordEraser is a Store that erases one tenant's capability records, for a consumer removing the tenant. It is optional; storetest checks a Store that implements it.

type TenantUsageEraser added in v0.60.0

type TenantUsageEraser interface {
	// EraseTenantUsage removes every counter, hold and charge of the
	// tenant's capabilities and its Biscuit copies, and the tenant's
	// aggregate: the charges ledger is the tenant's billing history, and
	// it does not come back. It changes no other tenant's rows.
	EraseTenantUsage(ctx context.Context, req EraseTenantUsageRequest) error
}

TenantUsageEraser is a UsageStore that erases one tenant's usage, for a consumer removing the tenant. It is optional; metertest checks a UsageStore that implements it.

type TokenType

type TokenType string

TokenType is a token format, as the JWT "typ" header names it.

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

TokenTypeV4 is the JWT "typ" header of the current token format, which every Signer of this module mints. The verifier requires a type it accepts (VerifierConfig.AcceptTokenTypes), 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 before a claim of it is read under the wrong schema.

v4 spells every ceiling out: an absent max_requests or max_budget is a ceiling of 0, and only unlimited_requests or unlimited_budget lifts it. v3, which read an absent bound as none, is not accepted.

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 is the unit the counters count in: the subject's, recorded
	// by the first Bump, Charge or Reserve of a subject that names one, so
	// a capability that has only been bumped reads its unit too. Empty
	// while none has.
	UnitCode string
	// contains filtered or unexported fields
}

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

type UsageHousekeeping

type UsageHousekeeping interface {
	// DeleteUsage drops the capability's row and the counters of its Biscuit
	// copies. 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. A capability with no row is no
	// error: there is nothing to drop.
	DeleteUsage(ctx context.Context, req DeleteUsageRequest) error

	// PurgeOrphans deletes the usage rows of capabilities no longer on
	// record (Store.Purge), with their copies' counters, releasing their
	// reservations as DeleteUsage does, at
	// most req.Limit of them; the caller loops while a batch comes back
	// full.
	PurgeOrphans(ctx context.Context, req PurgeOrphansRequest) (PurgeOrphansResult, error)

	// ReleaseExpired releases the reservations past their expiry, at most
	// req.Limit of them. 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, req ReleaseExpiredRequest) (ReleaseExpiredResult, error)
}

UsageHousekeeping reclaims usage rows of capabilities that are gone.

type UsageStore

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

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

func Instrument added in v0.12.0

func Instrument[TX any](inner UsageStore[TX], opts ...InstrumentOption) (UsageStore[TX], error)

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

The result implements every interface of this package that inner does — the optional CopyUsageReader and TenantUsageEraser — so wrapping a store never hides what it can do. A decorator forwards a fixed list of optional interfaces, and a test fails when the list misses one the store has.

type VerifiedCapability added in v0.14.0

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

VerifiedCapability is a capability a Verifier accepted. Only Verify and Authorize make one, so a function that takes it — Issuer.Delegate — knows the token it came from was checked, rather than trusting a caller to have checked it.

func (VerifiedCapability) Capability added in v0.14.0

func (v VerifiedCapability) Capability() Capability

Capability is what the token grants: a copy, the caller's to change.

func (VerifiedCapability) FromBiscuit added in v0.25.0

func (v VerifiedCapability) FromBiscuit() bool

FromBiscuit reports whether the capability was read from a copy of a Biscuit rather than from a JWT. Issuer.Delegate and Issuer.Biscuit take only the latter.

func (VerifiedCapability) MeterSubject added in v0.17.0

func (v VerifiedCapability) MeterSubject() MeterSubject

MeterSubject is what the Meter counts this capability's requests against.

type Verifier

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

Verifier is the production verifier — signature first, then claims, then issuer / tenant / time / audience / revocation gates. A deployment typically resolves keys through a cached JWKS (jwks.Remote) and checks revocation through a short-TTL cache (CachedRevocationChecker), so revocation propagates quickly without hammering the store. A consumer that mocks it declares the narrow interface it needs.

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

func NewVerifier added in v0.36.0

func NewVerifier(cfg VerifierConfig) (*Verifier, error)

NewVerifier validates wiring and builds the verifier.

func (*Verifier) Authorize added in v0.3.0

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.

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/dpop"
	"github.com/oleg-tkachuk/limes/jwks"
	"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.Verifier
	records  *memstore.Store
	usage    *memstore.UsageStore[struct{}]
	tenantID uuid.UUID
}

func newExampleStack() exampleStack {
	key, err := limes.GenerateKey()
	kid, pub, priv := key.KeyID, key.Public(), key.Private
	if err != nil {
		log.Fatal(err)
	}
	records, err := memstore.NewStore(memstore.StoreConfig{})
	if err != nil {
		log.Fatal(err)
	}
	usage, err := memstore.NewUsageStore(memstore.UsageStoreConfig[struct{}]{Records: records})
	if err != nil {
		log.Fatal(err)
	}
	signer, err := limes.NewSigner(limes.SignerConfig{KeyID: kid, Key: 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)
	}
	replay, err := dpop.NewMemoryReplayCache(dpop.MemoryReplayCacheConfig{})
	if err != nil {
		log.Fatal(err)
	}
	proofs, err := dpop.NewVerifier(dpop.VerifierConfig{Replay: replay})
	if err != nil {
		log.Fatal(err)
	}
	verifier, err := limes.NewVerifier(limes.VerifierConfig{
		Keys:               exampleKeys(kid, pub),
		Revocations:        records,
		TrustedIssuers:     []string{exampleIssuerName},
		AcceptBiscuit:      true,
		BiscuitRevocations: records,
		DPoP:               proofs,
	})
	if err != nil {
		log.Fatal(err)
	}
	return exampleStack{
		issuer: issuer, verifier: verifier, records: records,
		usage: usage, tenantID: uuid.New(),
	}
}

// verified is the capability token grants, as the verifier accepts it.
func (s exampleStack) verified(token string) limes.VerifiedCapability {
	vc, err := s.verifier.Verify(context.Background(), limes.VerifyRequest{Token: token, Audience: exampleAudience})
	if err != nil {
		log.Fatal(err)
	}
	return vc
}

// 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) {
	cResult, err := s.issuer.Issue(context.Background(), limes.IssueRequest{
		IssuedBy: limes.Principal{Subject: "operator@example.com"},
		Subject: limes.Principal{
			Type: limes.PrincipalTypeAgent, 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/"},
			Requests:         limes.RequestCeiling{Max: 500},
			Budget:           limes.BudgetCeiling{Max: limes.MustParseAmount("25.00"), UnitCode: "USD"},
		},
	})
	c, token := cResult.Capability, cResult.Token
	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{
			Verify: limes.VerifyRequest{Token: token, Audience: exampleAudience},
			Check:  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))
}

// exampleKeys is a jwks.Static holding pub under kid.
func exampleKeys(kid string, pub ed25519.PublicKey) *jwks.Static {
	keys, err := jwks.NewStatic(jwks.StaticConfig{Keys: map[string]ed25519.PublicKey{kid: pub}})
	if err != nil {
		log.Fatal(err)
	}
	return keys
}
Output:
<nil>
true
true

func (*Verifier) BiscuitCopy added in v0.36.0

func (v *Verifier) 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 (*Verifier) DPoPNonce added in v0.36.0

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

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

func (*Verifier) ErrorResponse added in v0.36.0

func (v *Verifier) 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 (*Verifier) Verify

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; a refusal is one of the package's 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 JWT costs a header decode and one signature check, and no claim-parsing path is reachable by an attacker. A Biscuit is read further before its signatures are checked: its container and every block are decoded and their facts read as data, which MaxTokenBytes and MaxBiscuitBlocks bound; none of it is trusted until the signatures have verified. The checks that need no I/O come before the revocation lookup — the proof of possession among them, so a holder of a key-bound token without its key learns nothing of the token's revocation and causes no lookup — and the proof's id is recorded in the replay cache after it, so a revoked token never records one.

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
	// ErrInvalidToken.
	TrustedIssuers []string

	// KeyIssuers 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. Empty binds no kid: enough with one
	// trusted issuer. With several it is required, unless SharedKeys says otherwise:
	// any trusted key signing for any trusted issuer is wrong for issuers
	// that do not share a trust boundary, so it is never the default.
	KeyIssuers map[string]string

	// SharedKeys lets every trusted key sign for every trusted issuer, for
	// several issuers that do share one trust boundary. It and KeyIssuers
	// exclude each other.
	SharedKeys bool

	// 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. Zero: DefaultLeeway; negative or past MaxLeeway
	// is refused.
	Leeway time.Duration
	// NoLeeway holds nbf and exp to the verifier's clock exactly, for an
	// issuer that shares it. It excludes a Leeway.
	NoLeeway bool

	// MaxTokenBytes rejects longer tokens before decoding them. Default
	// DefaultMaxTokenBytes when zero; negative is refused.
	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.Requests, Attenuation.Budget).
	// Set it only when the Meter that enforces caveats counts
	// the copies a MeterSubject carries; off, such a token is refused with
	// ErrCopyCountersNotMetered rather than accepted with its limits unkept
	// — once it has passed every other check, so a token refused for what
	// it is gets that refusal instead.
	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

	// AcceptTokenTypes are the token formats (the JWT "typ") the verifier
	// accepts. Empty: TokenTypeV4. Every 1.x verifier accepts TokenTypeV4,
	// so a later format ships in a minor release: verifiers accept it
	// first, issuers mint it after. A type this package has no schema for
	// is refused: a verifier cannot enforce a format it cannot read.
	AcceptTokenTypes []TokenType

	// 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 ErrProofUnsupported, rather than accept
	// one as a bearer token. Built by dpop.NewVerifier, with its own clock.
	DPoP *dpop.Verifier

	// Realm names the protection space in every WWW-Authenticate challenge
	// ErrorResponse gives (RFC 9110 §11.5). Empty: DefaultRealm. Only the
	// characters RFC 6750 §3 allows in an auth-param value.
	Realm string
}

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 dpop.Proof
	// 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
Package dpop implements proof of possession after RFC 9449 (DPoP): a client signs a short JWT over each request with a key it holds, and a server checks the proof against the key a token is bound to.
Package dpop implements proof of possession after RFC 9449 (DPoP): a client signs a short JWT over each request with a key it holds, and a server checks the proof against the key a token is bound to.
dpoptest
Package dpoptest checks a dpop.ReplayCache and a dpop.Nonces against the package's contract, the way storetest checks a limes.Store.
Package dpoptest checks a dpop.ReplayCache and a dpop.Nonces against the package's contract, the way storetest checks a limes.Store.
exchange module
Package inspect reads a capability token for tooling — a CLI that shows a token, a debugger, a log viewer — without verifying it.
Package inspect reads a capability token for tooling — a CLI that shows a token, a debugger, a log viewer — without verifying it.
internal
fields
Package fields lets the contract suites, storetest and metertest, prove that what they write and read back covers every field of the module's types.
Package fields lets the contract suites, storetest and metertest, prove that what they write and read back covers every field of the module's types.
httpx
Package httpx names the HTTP header fields and Cache-Control directives the module's packages write and read, so each is spelt in one place.
Package httpx names the HTTP header fields and Cache-Control directives the module's packages write and read, so each is spelt in one place.
jose
Package jose holds the JOSE pieces the module's packages share: the algorithm and key identifiers, base64url, the compact serialisation, and the public JWK with its RFC 7638 thumbprint.
Package jose holds the JOSE pieces the module's packages share: the algorithm and key identifiers, base64url, the compact serialisation, and the public JWK with its RFC 7638 thumbprint.
link
Package link lets the module's own packages reach what the root does not export: limes/inspect the token reader, and metertest, storetest, memstore and the tests of memstore and pgstore the constructors of a MeterSubject and a BiscuitCopy.
Package link lets the module's own packages reach what the root does not export: limes/inspect the token reader, and metertest, storetest, memstore and the tests of memstore and pgstore the constructors of a MeterSubject and a BiscuitCopy.
rfc3986
Package rfc3986 normalises a URI the way RFC 3986 §6.2.2 and §6.2.3 say, for comparing two spellings of one request URI.
Package rfc3986 normalises a URI the way RFC 3986 §6.2.2 and §6.2.3 say, for comparing two spellings of one request URI.
watchdog
Package watchdog bounds one check of a contract suite.
Package watchdog bounds one check of a contract suite.
Package jwks publishes and resolves the Ed25519 keys capability tokens are signed with, as an RFC 7517 JSON Web Key Set.
Package jwks publishes and resolves the Ed25519 keys capability tokens are signed with, as an RFC 7517 JSON Web Key Set.
jwkstest
Package jwkstest checks a limes.KeyResolver against the module's contract, the way storetest checks a limes.Store.
Package jwkstest checks a limes.KeyResolver against the module's contract, the way storetest checks a limes.Store.
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.UsageStore against the module's contract — its Meter, tenant budgets and housekeeping — the way testing/fstest checks an fs.FS.
Package metertest checks a limes.UsageStore against the module's contract — its Meter, tenant budgets and housekeeping — 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